整体架构
合适的架构可以提升可测试性。比如菱形对称架构的模块化和解耦特性使得系统各个部分可以独立进行单元测试。这不仅提高了测试的效率,还能够减少测试的依赖性,提高测试准确性。
代码设计
代码设计和可测试性有密切关联。强烈建议一个方法的代码行数不要太多。这样,如果需要细粒度单元测试(需要比较高的代码覆盖率的单元测试),就更容易。大不了把私有方法变成公有,加一
@VisibleForTesting注解来测试。
其他设计比如采用易于测试的设计模式:选择合适的设计模式可以显著提高代码的可测试性。例如,使用依赖注入(DI)模式可以使依赖关系在运行时动态绑定,便于模拟和替换依赖项,从而更容易进行单元测试。
测试框架
分布式微服务系统而言,“粗粒度单元测试”最大挑战在于如何解决周边依赖问题,对于一个典型的应用而言,它可能会依赖数据库、消息中间件、缓存系统等,以及周边的其它服务。如何处理这些依赖,是我们必须要面对和要解决的问题。
其实解决方案只有一种叫Test Double(测试替身),即要用“替身”来代替依赖,从而让程序在没有真正依赖环境的情况下,仍然可以运行,验证功能。这些“替身”有不少fancy的名字,比如dummy、fake、stub、mock、spy等,这里我就不啰嗦这些概念的差异了,也不重要。重要的是我们该如何高效的实施“粗粒度单元测试”。
Mock做替身
使用mock做“替身”是我们处理外部依赖的最常见做法,有很多的框架比如Mockito,EasyMock支持我们去做mock的事情。
这种方法有效,但存在两个问题:
比较繁琐,当需要构造的对象有大量字段的时候,需要额外写很多的代码。
有些功能测不全,比如这里的SQL语句就不能被测试到,因为数据库的访问是被mock掉的。
Embedded Server做替身
鉴于以上的问题,我发现很多团队都开始转向embedded方案,即在本地拉起一个真实的内嵌依赖服务,作为依赖的“替身”。比如我用到了Redis缓存,那么我可以使用embedded-redis启动一个本地的redis,从而连接到“真实”的redis环境。使用很简单,只需要加入下面的依赖就可以。在Junit5的Extension帮助下,我们可以很优雅的使用embedded的服务。
<dependency>
<groupId>com.github.codemonstur</groupId>
<artifactId>embedded-redis</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
类似的,我们常用的中间件比如kafka、MySQL、MongoDB等等都有相对应的embedded的方案。对于非中间件,比如其它服务依赖,我们可以使用wiremock来做其它服务API的“替身”。这些动作在很大程度上可以帮助我们消解依赖问题,这样做可以给我们带来以下好处:
在同样代码覆盖率的情况下,使用embedded的集成测试方案,可以写更少的测试用例和代码,研发效率更高
因为使用的是真实中间件服务,可以让我们的测试更加贴近真实环境,测试的有效性更高。
测试的入口是接口。这种更大测试粒度带来的好处是,可以帮助我们更好的重构内部业务逻辑,而不用担心破坏测试用例,如果UT太细的话,重构代码的同时还要重写UT,比较麻烦。
你也许会说,和Mock方案相比,这里的数据准备并没有比Mock少多少啊,实际上的确如此,对于测试数据,我们可以使用测试夹具(Test Fixtures)把测试数据用json/xml的形式放在resource下面,这样可以提升我们的数据复用和数据准备效率。
除此之外,embedded方案还引入了一个比较严重的问题——慢!即在运行测试的时候,需要拉起整个环境,整个过程很耗时,一般情况下,如果要启动3个embedded服务,短则一分钟多则几分钟,这显然与我们所期望的单测要“短平快”不相符。“慢”会极大的抑制我们运行UT的积极性,也就不能享受UT带来的quick feedback好处,更不用说TDD了。