前言
单元测试是指对软件中最小可测单元进行检查和验证;c语言中单元指一个函数,java中指一个类。图形化软件中可以指一个窗口或者一个菜单。总的来说,单元就是认为规定最小的被测试模块。
1.1单元测试对我们开发程序有什么好处
首先是一个前端单元测试的根本性原由:JavaScript 是动态语言,缺少类型检查,编译期间无法定位到错误; JavaScript 宿主的兼容性问题。比如 DOM 操作在不同浏览器上的表现。
正确性:测试可以验证代码的正确性,在上线前做到心里有底。
自动化:当然手工也可以测试,通过console可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
解释性:测试用例用于测试接口、模块的重要性,那么在测试用例中就会涉及如何使用这些API。其他开发人员如果要使用这些API,那阅读测试用例是一种很好地途径,有时比文档说明更清晰。
驱动开发,指导设计:代码被测试的前提是代码本身的可测试性,那么要保证代码的可测试性,就需要在开发中注意API的设计,TDD将测试前移就是起到这么一个作用。
保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构。
规范
2.1 单元测试主体
大多数单元测试包括四个主体:
测试套件describe、
测试用例it、
判定条件expect、
断言结果toEqual。
2.2 单元测试用例的原则
测试代码时,只考虑测试,不考虑内部实现;
数据尽量模拟现实,越靠近现实越好,
充分考虑数据的边界条件下·
对重点、复杂、核心代码、重点测试
利用AOP(面向切面编程),减少测试代码,避免无用功能
测试、功能开发相结合,有利于设计和代码重构
常用框架
3.1 Spock
Spock主要特点如下:
让测试代码更规范,内置多种标签来规范单元测试代码的语义,测试代码结构清晰,更具可读性,降低后期维护难度。
提供多种标签,比如:given、when、then、expect、where、with、thrown……帮助我们应对复杂的测试场景。
使用Groovy这种动态语言来编写测试代码,可以让我们编写的测试代码更简洁,适合敏捷开发,提高编写单元测试代码的效率。
遵从BDD(行为驱动开发)模式,有助于提升代码的质量。
IDE兼容性好,自带Mock功能。
3.1.1 为什么使用Spock? Spock和JUnit、jMock、Mockito的区别在哪里?
总的来说,JUnit、jMock、Mockito都是相对独立的工具,只是针对不同的业务场景提供特定的解决方案。其中JUnit单纯用于测试,并不提供Mock功能。
我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储比如Squirrel、DB、MCC配置中心等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。
尽管jMock、Mockito提供了Mock功能,可以把接口等依赖屏蔽掉,但不能对静态方法Mock。虽然PowerMock、jMockit能够提供静态方法的Mock,但它们之间也需要配合(JUnit + Mockito PowerMock)使用,并且语法上比较繁琐。工具多了就会导致不同的人写出的单元测试代码“五花八门”,风格相差较大。
Spock通过提供规范性的描述,定义多种标签(given、when、then、where等),去描述代码“应该做什么”,“输入条件是什么”,“输出是否符合预期”,从语义层面规范了代码的编写。
Spock自带Mock功能,使用简单方便(也支持扩展其他Mock框架,比如PowerMock),再加上Groovy动态语言的强大语法,能写出简洁高效的测试代码,同时能方便直观地验证业务代码的行为流转,增强工程师对代码执行逻辑的可控性。
3.1.2使用Spock解决单元测试开发中的痛点
如果在(if/else)分支很多的复杂场景下,编写单元测试代码的成本会变得非常高,正常的业务代码可能只有几十行,但为了测试这个功能覆盖大部分的分支场景,编写的测试代码可能远不止几十行。
之前有遇到过某个功能上线很久一直都很正常,没有出现过问题,但后来有个调用请求的数据不一样,走到了代码中一个不常用的逻辑分支时,出现了Bug。当时写这段代码的同学也认为只有很小几率才能走到这个分支,尽管当时写了单元测试,但因为时间比较紧张,分支又多,就漏掉了这个分支的测试。
尽管使用JUnit的@Parametered参数化注解或者DataProvider方式可以解决多数据分支问题,但不够直观,而且如果其中某一次分支测试Case出错了,它的报错信息也不够详尽。
这就需要一种编写测试用例高效、可读性强、占用工时少、维护成本低的测试框架。首先不能让业务人员排斥编写单元测试,更不能让工程师觉得写单元测试是在浪费时间。而且使用JUnit做测试工作量不算小。据初步统计,采用JUnit的话,它的测试代码行和业务代码行能到3:1。如果采用Spock作为测试框架的话,它的比例可缩减到1:1,能够大大提高编写测试用例的效率。
3.2 Mockito
3.2.1什么是 Mockito
Mockito 是一个强大的用于 Java 开发的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试.
使用 Mockito 的大致流程如下:
创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中.
执行测试代码.
校验测试代码是否执行正确
3.2.2为什么使用 Mockito
假设我们正在编写一个银行的服务 BankService, 这个服务的依赖关系如下:
当我们需要测试 BankService 服务时, 该真么办呢?
一种方法是构建真实的 BankDao, DB, AccountService 和 AuthService 实例, 然后注入到 BankService 中.
不用我说, 读者们也肯定明白, 这是一种既笨重又繁琐的方法, 完全不符合单元测试的精神. 那么还有一种更加优雅的方法吗?
自然是有的, 那就是我们今天的主角 Mock Object. 下面来看一下使用 Mock 对象后的框架图:
我们看到, BankDao, AccountService 和 AuthService 都被我们使用了虚拟的对象(Mock 对象) 来替换了, 因此我们就可以对 BankService 进行测试, 而不需要关注它的复杂的依赖了.
3.3 JUnit5
3.3.1 JUnit 5架构体系
作为最新版本的JUnit框架,JUnit 5相比之前版本的JUnit框架有了较大的突破,添加了许多新特性。
之前的JUnit 框架所有的功能都被打包在一个构件(artifact)中。它被提供给开发者、IDE、构建工具、其他测试框架、其他扩展等使用,不同的使用者,依赖的都是一个同样的jar包。
而JUnit 5 最重要的一大优化就是其架构体系,它由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform 其主要作用是在 JVM 上启动测试框架。它定义了一个抽象的 TestEngine API 来定义运行在平台上的测试框架,同时还支持通过命令行、Gradle 和 Maven 来运行平台。通过 JUnit Platform,其他的自动化测试引擎或开发人员自己定制的引擎都可以接入 Junit 实现对接和执行
JUnit Jupiter 包含了 JUnit 5 最新的编程模型和扩展机制; Jupiter 本身也是一个基于 Junit Platform 的引擎实现。
JUnit Vintage 兼容JUnit3,JUnit4 版本的测试引擎
再来看下JUnit 5的架构图
我们来进一步看看各个子项目的作用。
第一层 : 开发人员(这里只进行业务开发撰写单元测试) 使用junit-jupiter-api等测试框架api编写单元测试
第二层 : 测试引擎,JUnit 或其他测试框架实现引擎API的框架,jupiter-engine和 vintage-engine分别是junit 4和junit 5 对测试引擎API的实现,其他的测试框架也可以通过实现引擎API从而接入JUnit 平台
第三层: 平台引擎 junit-platform-engine 是上一层各种引擎实现的抽象,即引擎的接口标准。
第四层: 启动器 通过ServiceLoader发现测试引擎的实现并安排其执行。 它为IDE和构建工具提供了API,因此IDE可以与测试执行交互,例如,通过启动单个测试并显示其结果。
3.3.2 JUnit 5的新特性
包可见性
在JUnit 4里我们的测试方法必须定义为public的访问级别,如果没有定义成public,虽然编译的时候不会提示异常,但是在运行时会提示 “java.lang.Exception: Method testIsBlank() should be public” 如下错误信息。
而在JUnit 5里,我们不再需要将测试类与测试方法定义为public了,默认的包可见的访问级别就可以了。
常用注解
对于在JUnit 4中的常用注解,你都可以在JUnit 5中找到对应的注解,关系如下:
JUnit 5JUnit 4说明
@Test@Test指明被注解的方法是一个测试方法,注意JUnit 5的@Test在jupiter-api里
@BeforeAll@BeforeClass被注解的静态方法会在当前类的所有@Test方法执行前执行一次
@BeforeEach@Before被注解的方法会在当前类的每个@Test方法执行前执行一次
@AfterAll@AfterClass被注解的静态方法会在当前类的所有@Test方法执行后执行一次
@AfterEach@After被注解的方法会在当前类的每个@Test方法执行后执行一次
@Disabled@Ignore被注解的方法不会被执行,但是在测试报告里会记录为已执行
测试命名
写测试用例的时候,为了更好的可读性,我们往往会给测试方法定义一个有意义的名字,eg.testXXXWhenXXXThenReturnXXX。JUnit 5还提供了一个@DisplayName 注解,方便我们为每个测试用例添加更具体的名字,更容易表述用例所要测试的内容(可以是字符串,特殊符号,甚至是表情符号)。
断言
junit 框架中最常用的断言就是检查一个对象或者属性是否为null.或者判断两个属性是否一致。JUnit 4和JUnit 5中的断言方法都可以接受字符串作为一个可选参数,如果断言失败,则会在控制台输出对应的描述信息。JUnit 5中还可以使用 lambda 表达式 来构建这个描述信息。
注意一下,在JUnit 4和JUnit 5中描述信息的参数位置是不一样的。
tag标记
JUnit 5新增了@Tag注解,可以为测试类或方法添加标签,并在执行时快速地根据标签来针对性地运行测试。
扩展机制
在JUnit 5出来之前,我们如果想对JUnit 4 的核心功能进行扩展,往往都会使用自定义Runner 和 @Rule。
自定义Runner 通常是 BlockJUnit4ClassRunner 的子类,用于实现 JUnit 中没有直接提供的某种功能。 eg.spring-test框架的SpringJUnit4ClassRunner, 和mock框架的MockitoJUnitRunner
局限性:
必须在测试类级别上使用 @RunWith 注解来声明 Runner 。
@RunWith仅接受一个参数: Runner 的实现类。因为每个测试类最多只能拥有一个 Runner ,所以每个测试类最多也只能拥有一个扩展点。(在PowerMock中引入了@PowerMockRunnerDelegate,可以同时使用两个Runner)
为了解决 Runner 的限制,JUnit 4.7 引入了 @Rule 。一个测试类可声明多个 @Rule ,这些规则可在类级别和测试方法级别上运行,但是它只能在测试运行之前或之后执行指定操作。如果我们想在此之外的时间点进行扩展,@Rule也无法满足我们的要求。
JUnit 5扩展机制的核心准则:
Prefer extension points over features
基于这一准则,JUnit 5 中定义了许多扩展点,每个扩展点都对应一个接口。我们可以定义自己的扩展可以实现其中的某些接口,然后通过 @ExtendWith 注解注册给 JUnit,后者会在特定的时间点调用注册的接口实现。
参考:
Spock单元测试框架介绍以及在美团优选的实践
学习单元测试 Mockito
Mockito详细教程
关于JUnit5 你必须知道的(一)
关于JUnit5 你必须知道的(二)
关于JUnit5 你必须知道的(三)