再谈 API 的撰写 - 子系统

在做一个系统时,有一些子系统几乎是必备的:配置管理,CLI,以及测试框架。

配置管理

我们先说配置管理。一个系统的灵活度,和它的配置管理是离不开的。系统中存在的大量的预置的属性(下文简称 property),需要有一个公共的地方来放置。这里我不说「常量」,而是说「预置的属性」,是因为这属性可能需要在运行时发生改变,而常量的范畴会让人有所误解。

最简单的配置管理就是把所有的 property 放在一个配置文件中,在系统启动的时候读入。配置文件的类型有很多选择:ini,json,yaml,toml 等。这些类型各有优劣,选择的时候注意配置文件最好能够支持注释,便于维护。从这个角度看,json 不是个太好的选择。toml 可能大家用得不多,它是 github 创始人 Tom 定义的一种格式,类似于 ini 但灵活不少,感兴趣的可以在 github 里搜索 toml。

我们知道,一个项目会有多种运行时:development,staging,production,test。不同的运行时加载的配置文件可能不同。所以配置管理需要考虑这一点,让配置文件可以重载(override)。最常见的重载策略是系统提供一个公共的配置文件:default,然后各种运行时相关的配置文件继承并局部重载这个配置。在系统启动的时候,二者合并。

有些时候,我们需要在系统运行的时候改写配置。由于配置一般在系统初始化的时候就被读入内存,所以单纯改写配置文件无法即时生效,这时,你需要像管理缓存一样去管理和配置相关的数据,将其封装在一个容器里:当配置被修改时,调用这个容器的 invalidate 方法 —— 这样,下次访问任意一个配置项时,会重新读入配置,并缓存起来。

对于分布式的项目,配置应该集中存储在诸如 redis 这样的系统,以方便统一处理(orchestrate)。

CLI

写 CLI 并非难事,但一个 CLI 子系统的难点是:

  • CLI 的发现和自注册。你的 framework 的用户只要遵循某种 convention 撰写 CLI,这些 CLI 就会被自动集成到系统里。
  • CLI 的撰写者能够轻松地获取到系统的信息,也就是说,系统有自省(introspection)的能力。

前者的实现我们在前面的篇章里(谈谈编译和运行)讲路由是如何注册的已经提到,这里就不赘述。后者非常重要,在展开讨论之前,我们先考虑一个问题:做一个系统的过程中,我们希望这个系统的 CLI 解决什么问题?

首先,CLI 显然不是给用户用的,是给程序员用的,所以,CLI 提供一些简化程序员工作的脚本。那么,作为一个 API 系统,程序员都需要哪些 CLI 呢?我们看一些例子:

  • 创建某些 skeleton - rails / django 都有新建项目,新建 model / controller 等的 CLI
  • 获取系统的信息。比如:不用看代码就能很快知道系统里都有哪些 route,哪些 model 等。
  • 生成某些信息或者模拟某些行为。当你调试你的系统时,每次生成某种状态很烦人,比如说登录,可以通过 CLI 一键完成。
  • ...

这些例子大部分都需要系统的自省的能力,比如说下面这个 CLI:

在这个 CLI 的执行函数里,我们使用了这些系统信息:

  • 系统中所有 app 的名字。
  • 某个 app 的 instance。
  • app 的配置信息。
  • app 启用的 middleware。
  • app 的所有的路由。
  • ...

如果我们无法在系统的非运行时获取这些信息,那么,CLI 的威力会大打折扣。这也印证了我之前的文章 里所述的将「编译时」和「运行时」分开的重要性。很多框架,如 express.js,由于无法很清晰地将二者区分开,以至于你想在非运行的时刻获取 route / middleware 的信息,非常困难。

测试框架

API 的测试是相当无趣的(几乎所有的测试例撰写起来都相当无趣),但是测试的重要性是不容置疑的,尤其对于一个不断重构的代码。如果说别的系统的测试只能在局部寻找规律而进行优化,API 的测试,尤其是 functional testing 是可以全局考虑的。

比如你有一个 API 是 PUT /feature/:id,要测试这个 API 是否工作正常,你大概会考虑这些测试例:

  • PUT 正确的数据到一个错误的 id,测试是否会出错;
  • PUT 错误的 etag,测试 concurrent udpate 是否工作;
  • PUT 空数据,测试 validator 是否正常工作;
  • PUT 错误的数据,测试 validator 是否正常工作;
  • PUT 正常的数据,测试基本功能是否工作。

这些测试例有这些共同之处:

  • 需要运行一个 temporary server
  • 需要发送请求到 temporary server 上
  • 需要检测 status code,以及 response header / body 来确认是否出现期待的结果

如果每个测试都写一个测试例,虽然每个的代码量并不太大,但测试一个 API 就需要 5 个测试,API 的规模一上,代码量就大了,添加和维护都很麻烦。

我们可以定义一种针对于此的测试语言来描述测试的 fixture:

这个定义非常简单,相信大家都能看明白:

  • 测试的描述(用于 test report)
  • 测试所用的 url
  • 期待的结果,包括 status code,headers 和 body

这里面,我们用了一种很简单的方式区分 field name 和函数。比如 body 下面的 #length,它的结果不是body['length'] 而是 length(body),前者虽然对 array 有效(javascript),但对 object 无效。

要运行这样的 fixture,并不需要撰写太多的代码(假设我们是用 ava 作为测试工具):

这里面,runAssertion 发送 request,并对比 fixture 里面的数据和 response,来确定一个 test case pass 或者 fail。

这样下来,我们成功地把繁琐的 test case 的撰写转化成一个 parser 和一系列 fixture 的撰写。parser 的撰写是一次性的,以后改动很少(但会添加新的功能,比如新的函数 - 如上的 #xxx),而 fixture 的撰写对比着之前的例子,几乎很难出错。这样的测试例,你三五分钟写出一个来是轻而易举的事情。

原文发布于微信公众号 - 程序人生(programmer_life)

原文发表时间:2016-03-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JavaEE

spring整合quartz框架前言:quartz简介:spring整合quartz:总结:

在一些项目中,往往需要定时的去执行一些任务,比如商城项目,每天0点去统计前一天的销量。那么如何实现呢,总不可能我们每天0点手动的去执行统计销量的方法吧,这时就q...

451
来自专栏小白的技术客栈

两个微型的函数例子

hello小伙伴们大家好,还记得昨天的函数参数的文章吗?你get到了多少呢?实际的工作中并没有那么复杂,一个通用的函数定义形如: def func_name(*...

3235
来自专栏用户2442861的专栏

Android游戏开发十日通(6)- 太空大战

http://blog.csdn.net/silangquan/article/details/16921035

582
来自专栏jeremy的技术点滴

docker源码分析-Client创建与命令执行

2792
来自专栏用户1191492的专栏

JClouds的命令行界面

我已经使用JCloud(一种面向Java支持多种云的工具集)一年了。到目前为止,我已经在很多领域广泛地使用了JCloud,特别是在Fuse Eco...

2309
来自专栏Golang语言社区

Go语言写文件几种方式性能对比

方式2比方式1略快,但是重复次数多了后会报错,应该是defer被压栈太多导致系统撑不了太多打开的文件

662
来自专栏智能合约

crontab定时任务详解

1434
来自专栏技巅

Jvm(jdk8)源码分析1-java命令启动流程详解

1523
来自专栏影子

springboot添加邮件发送及压缩功能

转载请注明出处:https://www.cnblogs.com/funnyzpc/p/9190233.html

681
来自专栏Golang语言社区

转--在学Go语言

开始学点儿Go语言,这语言据说在国内比在国外火,社区上褒贬不一,不过“小马过河”嘛,总要先自己试试再来下结论。 环境准备: 1.在Golang中国下载安装go语...

3337

扫描关注云+社区