在上一篇文章里,我们介绍了测试金字塔以及如何将它应用在分布式系统里。
这篇文章将关注测试金字塔里的单元测试层,并探讨如何高效地为分布式系统(如微服务)构建单元测试。
定义测试边界
定义测试边界是实现高效测试的第一步。测试的目的是为了验证边界里“黑盒”的行为是否符合预期,我们向黑盒输入数据,然后验证输出的正确性。
在单元测试里,黑盒指的是函数或者类的方法,目的是单独测试特定代码块的行为。为了更好地理解这个概念,我们以简单的注册功能为例:
我们可以看到这个函数包含了一些输入和输出。这个函数接受基本的用户注册信息作为输入参数,并返回新创建的用户ID。
不过这里也有一些不是很明显的输入数据。这个函数调用了另个外部函数:一个向数据库插入数据,一个对密码进行散列和持久化。在某些情况下,数据库可能会返回错误。比如,因为用户名唯一性问题导致数据库插入失败,又或者需要通过调用外部的微服务进行密码散列,如果网络连接出现问题或密码散列服务因发生过载导致服务超时,那么密码散列函数就会返回错误。
为了全面测试用户注册功能,单元测试所要做的不仅仅是简单地传进去不同的输入参数,它还要能够让外部依赖项能够使用这些输入来验证函数的行为是否符合预期。在测试函数的错误处理逻辑时,这点很重要的。
Stub 和 Mock
为了制造各种输入数据,需要使用 stub,也叫作 mock。这个可以使用依赖注入或方法搅拌(swizzle)来实现。测试框架在运行被测试的函数时可以确保对底层依赖项的调用会被重定向到 stub 上:
我们可以使用 stub 来达到各种目的:
-
stub 可以什么事也不做。这样可以加快个别单元测试的速度,如果后续有其他单元测试可用于测试边界情况的话就可以这样做。
-
stub 可返回任意的值,用于模拟外部函数的输出。这在测试罕见的边界情况时会非常有用,比如有些错误场景很少会发生或者难以重现。
-
stub 也可以用于捕捉被测试函数欲传给外部函数的参数,或者把这些参数记录下来。这样就可以验证被测试函数需要调用哪些外部函数以及需要传给外部函数哪些参数。
测试分布式系统需要有一套很好的 stub,有了这些 stub,单元测试才能够在没有外部服务的情况下运行。下面列出了一些工具,用于创建各种 stub。
Node.js/JavaScript
-
sinon.js (提供了 stub 和间谍功能)
-
testdouble.js (主要用于面向对象 API 的 stub 生成器)
-
nock (主要用于模拟 HTTP 请求行为)
Python
Go
Java
单元测试流程
单元测试的目的是为了给开发人员提供快速验证他们所写代码的行为。因为对外部依赖的调用使用了 stub,所以通常可以在几秒钟内就可以执行数千个单元测试。所以,开发人员可以把单元测试加入到他们的开发工作流当中,要么直接集成到他们的 IDE 里,要么通过终端命令行来运行。开发人员在编写代码的同时频繁地运行单元测试可以帮助他们及早地发现代码中的问题。
一旦开发人员养成了这样的习惯,那么就可以进行测试驱动开发了。开发人员在开发新特性之前会先准备好单元测试,在新特性被加进来之前,测试总是失败。在经过不断的测试和代码修改之后,一个完整的功能被开发出来了,最后再运行测试就能通过。
单元测试的作用不应局限于代码开发,它们也应该被集成到代码合并流程里。GitHub 支持一些主流持续集成服务器的状态检查。一般的流程是这样的:保护好“master”分支,不允许开发人员向该分支提交代码,而是让他们把代码提交到其他分支上。在将代码合并到 master 分支的时候,GitHub 要求先通过状态检查。
Jenkins、CircleCI 和 TravisCI 都提供了状态检查钩子(hook),它们会从分支上获取代码并运行单元测试。如果通过了,就允许合并代码,否则就不允许。
总结
单元测试是测试工具箱里的一个非常重要的工具。为了对分布式系统代码进行全面的单元测试,有必要利用一些支持 stub 的测试框架,用于模拟各种错误场景或外部依赖的各种响应。
查看英文原文: Microservice Testing: Unit Tests
评论