你好,我是茹炳晟,今天我和你分享的主题是“知其然知其所以然:聊聊 API 自动化测试框架的前世今生”。

在上一篇文章中,我以一个简单的 Restful API 为例,分别介绍了 cURL 和 Postman 的使用方法,相信你已经对 API 测试有个感性认识了。

但是,我们不能仅仅停留在感性认识的层面,还需要熟悉并掌握这些测试方法,完成相应的 API 测试工作。所以,也就有了我今天分享的主题,希望可以通过对 API 自动化测试框架发展的介绍,让你理解 API 测试是如何一步一步地发展成今天的样子,以“知其所以然”的方式加深你对 API 自动化测试的理解。

接下来,我将会遵循由简入繁的原则,为你介绍 API 测试框架,以发现问题然后解决问题的思路为主线,展开今天的分享。

早期的基于 Postman 的 API 测试

早期的 API 测试,往往都是通过类似 Postman 的工具完成的。但是,由于这类工具都是基于界面操作的,所以有以下两个问题亟待解决:

  1. 当需要频繁执行大量的测试用例时,基于界面的 API 测试就显得有些笨拙;
  2. 基于界面操作的测试难以与 CI/CD 流水线集成。

所以,我们迫切需要一套可以基于命令行执行的 API 测试方案。这样,API 测试可以直接通过命令行发起,与 CI/CD 流水线的整合也就方便得多了。

基于 Postman 和 Newman 的 API 测试

于是就出现了集成 Postman 和 Newman 的方案,然后再结合 Jenkins 就可以很方便地实现 API 测试与 CI/CDl 流水线的集成。Newman 其实就是一个命令行工具,可以直接执行 Postman 导出的测试用例。

用 Postman 开发调试测试用例,完成后通过 Newman 执行,这个方案看似很完美。但是在实际工程实践中,测试场景除了简单调用单个 API 以外,还存在连续调用多个 API 的情况。

此时,往往会涉及到多个 API 调用时的数据传递问题,即下一个 API 调用的参数可能是上一个 API 调用返回结果中的某个值。另外,还会经常遇到的情况是,API 调用前需要先执行一些特定的操作,比如准备测试数据等。

因此,对于需要连续调用多个 API 并且有参数传递的情况,Postman+Newman 似乎就不再是理想的测试方案了。

基于代码的 API 测试

为了解决这个问题,于是就出现了基于代码的 API 测试框架。比较典型的是,基于 Java 的 OkHttP 和 Unirest、基于 Python 的 http.client 和 Requests、基于 NodeJS 的 Native 和 Request 等。

小型的互联网企业,往往会根据自己的业务需求,选用这些成熟的 API 测试框架。

但是,对于中大型的互联网企业,一般都会自己开发更适合自身业务上下文的 API 测试框架,比如 eBay,我们为了实现代码化的 API 测试,开发了自己的 HttpClient,后期为了使 API 测试的代码更简洁易懂,就基于 Rest-Assured 封装了全新的 API 测试框架。

这种根据公司业务上下文开发实现的 API 测试框架,在使用上有很多优点,而且灵活性也很好,主要体现在以下几个方面:

  1. 可以灵活支持多个 API 的顺序调用,方便数据在多个 API 之间传递,即上一个 API 调用返回结果中的某个字段值可以作为后续 API 调用的输入参数;
  2. 方便在 API 调用之前或者之后执行额外的任意操作,可以在调用前执行数据准备操作,可以在调用后执行现场清理工作等;
  3. 可以很方便地支持数据驱动测试,这里的数据驱动测试概念和 GUI 测试中的数据驱动测试完全相同,也就是可以将测试数据和测试代码分离解耦;
  4. 由于直接采用了代码实现,所以可以更灵活地处理测试验证的断言(Assert);
  5. 原生支持命令行的测试执行方式,可以方便地和 CI/CD 工具做集成。

这里我给出了一段伪代码示例,用于展示如何用代码实现一个简单的 API 测试。

图 1 基于代码的 API 测试的伪代码示例

  • 代码的第 1-12 行,创建了 CreateUserAPI 类,其中包含了 endpoint、操作方法 PUT、InlineParam 和 Param 的设置,并且构建了对应的 request 对象;
  • 代码的第 14-19 行,是测试的主体函数。这段函数的逻辑是这样的:
    • 首先,构建 CreateUserAPI 的对象;
    • 然后,用 CreateUserAPI 对象的 buildRequest 方法结合输入参数构建 request 对象;
    • 接着,通过 request 对象的 request() 方法发起了 API 调用;
    • 最后,验证 response 中的状态码是不是 200。

在这段伪代码中,有以下几点需要你特别注意:

  1. 代码中“CreateUserAPI 的父类 RestAPI”“_buildRequest() 方法”“request() 方法”“addInlineParam() 方法”等,都是由 API 测试框架提供的。
  2. 为了简化代码,这里并没有引入数据驱动的 data provider。但在实际项目中,代码第 14 行的测试输入参数,往往来自于 data provider,即由数据驱动的方式提供测试输入数据。
  3. 由于测试过程完全由代码实现,所以可以很方便的在测试执行前后增加任意的额外步骤。比如,需要在 CreateUser 前增加数据创建的步骤时,只需要在代码第 15 行前直接添加就可以了。
  4. 这里的例子只有一个 API 调用,当需要多个 API 顺序调用时,直接扩展 testCreateUser 方法即可,两个 API 之间的数据传递可以通过上一个 API 返回的 response.XXXX 完成。

通过这段伪代码,我们可以看到,虽然基于代码的 API 测试灵活性很好,也可以很方便地和 CI/CD 集成,但是也引入了一些新的问题,比如:

  • 对于单个 API 测试的场景,工作量相比 Postman 要大得多;
  • 对于单个 API 测试的场景,无法直接重用 Postman 里面已经积累的 Collection。

在实际工程中,这两个问题非常重要,而且必须要解决。因为公司管理层肯定无法接受相同工作的工作量直线上升,同时原本已经完成的部分无法继续使用,所以自动化生成 API 测试代码的技术也就应运而生了。

自动生成 API 测试代码

自动生成 API 测试代码是指,基于 Postman 的 Collection 生成基于代码的 API 测试用例。

其实,在上一篇文章《从 0 到 1:API 测试怎么做?常用 API 测试工具简介》最后的部分,我已经提到过 Postman 工具本身已经支持将 Collection 转化成测试代码,但如果直接使用这个功能的话,还有两个问题需要解决:

  1. 测试中的断言(assert)部分不会生成代码,也就是说测试代码的生成只支持发起 request 的部分,而不会自动生成测试验证点的代码;
  2. 很多中大型互联网企业都是使用自己开发的 API 测试框架,那么测试代码的实现就会和自研 API 测试框架绑定在一起,显然 Postman 并不支持这类代码的自动生成。

鉴于以上两点,理想的做法是自己实现一个代码生成工具,这个工具的输入是 Postman 中 Collection 的 JSON 文件,输出是基于自研 API 框架的测试代码,而且同时会把测试的断言一并转化为代码。

这个小工具实现起来并不复杂,其本质就是解析 Collection JSON 文件的各个部分,然后根据自研 API 框架的代码模板实现变量替换。 具体来讲,实现过程大致可以分为以下三步:

  • 首先,根据自研 API 框架的代码结构建立一个带有变量占位符的模板文件;
  • 然后,通过 JSON 解析程序,按照 Collection JSON 文件的格式定义去提取 header、method 等信息;
  • 最后,用提取得到的具体值替换之前模板文件中的变量占位符,这样就得到了可执行的自研框架的 API 测试用例代码。

有了这个工具后,我建议你的工作模式(Working Model)可以转换成这样:

  • 对于 Postman 中已经累积的 Collection,全部由这个工具统一转换成基于代码的 API 测试用例;
  • 开发人员继续使用 Postman 执行基本的测试,并将所有测试用例保存成 Collection,后续统一由工具转换成基于代码的 API 测试用例;
  • 对于复杂测试场景(比如,顺序调用多个 API 的测试),可以组装由工具转换得到的 API 测试用例代码,完成测试工作。

如图 2 所示,就是一个组装多个由工具转换得到的 API 测试用例代码的例子。其中,代码第 3 行的类“CreateUserAPI”和第 10 行的类“BindCreditCardAPI”的具体代码就可以通过工具转换得到。

图 2 多个 API 顺序调用的测试用例代码

至此,基于代码的 API 测试发展得算是比较成熟了,但在实际应用过程中还有一个痛点一直未被解决,那就是测试验证中的断言,也是我接下来要和你一起讨论的话题。

Response 结果发生变化时的自动识别

在实际的工程项目中,开发了大量的基于代码的 API 测试用例后,你会发现一个让人很纠结的问题:到底应该验证 API 返回结果中的哪些字段?

因为你不可能对返回结果中的每一个字段都写 assert,通常情况下,你只会针对关注的几个字段写 assert,而那些没写 assert 的字段也就无法被关注了。

但对 API 测试来说,有一个很重要的概念是后向兼容性(backward compatibility)。API 的后向兼容性是指,发布的新 API 版本应该能够兼容老版本的 API。

后向兼容性除了要求 API 的调用参数不能发生变化外,还要求不能删减或者修改返回的 response 中的字段。因为这些返回的 response 会被下游的代码使用,如果字段被删减、改名或者字段值发生了非预期的变化,那么下游的代码就可能因为无法找到原本的字段,或者因为字段值的变化而发生问题,从而破坏 API 的后向兼容性。

所以,我们迫切需要找到一个方法,既可以不对所有的 response 字段都去写 assert,又可以监测到 response 的结构以及没有写 assert 的字段值的变化。

在这样的背景下,诞生了“Response 结果变化时的自动识别”技术。也就是说,即使我们没有针对每个 response 字段都去写 assert,我们仍然可以识别出哪些 response 字段发生了变化。

具体实现的思路是,在 API 测试框架里引入一个内建数据库,推荐采用非关系型数据库(比如 MongoDB),然后用这个数据库记录每次调用的 request 和 response 的组合,当下次发送相同 request 时,API 测试框架就会自动和上次的 response 做差异检测,对于有变化的字段给出告警。

你可能会说这种做法也有问题,因为有些字段的值每次 API 调用都是不同的,比如 token 值、session ID、时间戳等,这样每次的调用就都会有告警。

但是这个问题很好解决,现在的解决办法是通过规则配置设立一个“白名单列表”,把那些动态值的字段排除在外。

总结

为了让你可以更好地理解今天的 API 测试框架,我从其发展历程的角度进行了分析:

早期的基于 Postman 的 API 测试在面临频繁执行大量测试用例,以及与 CI/CD 流水线整合的问题时,显得心有余而力不足。为此,基于命令行的 API 测试实践,也就是 Postman+Newman,具有很好的灵活性,解决了这两个问题。

但是,Postman+Newman 的测试方案,只能适用于单个 API 调用的简单测试场景,对于连续调用多个 API 并涉及到参数传递问题时,这个方案就变得不那么理想和完美了。随后,API 测试就过渡到了基于代码的 API 测试阶段。

一些小型企业,则往往会选择适合自己业务的成熟 API 测试框架。中大型的互联网企业,一般都会根据自己的业务上下文,在成熟 API 测试框架的基础上封装自己的 API 测试框架,提升测试效率和灵活性。

但是,不管是采用现成的还是自己去开发 API 测试框架,都会遇到测试用例开发效率低下,以及无法直接重用 Postman 中积累的 Collection 的问题,为此我分享了两个比较好用的方法,也就是:自动生成 API 测试代码和 Response 结果变化的自动识别,并给出了这两个方法的实现思路。

希望我分享的这些内容,可以帮你解决在实际测试项目中遇到的问题。

思考题

目前,基于代码的 API 测试框架已经比较成熟了,所以在此基础上又出现了基于配置文件的 API 测试框架,比如典型的 HttpRunner,在此类 API 测试框架的支持下,测试用例本身往往就是纯粹的配置文件了。你是否有接触过这类 API 测试框架,对此又有什么看法呢?

欢迎你给我留言。