单元测试指的是对软件系统中的最小可测单元进行测试和验证。在面向对象的程序设计中,一个类中最小可执行单元通常是作为一个函数或者方法,单元测试要尽可能的对所有的方法覆盖,对预期结果和实际结果进行验证。在这里首先要感谢我现在所在公司给与的培训,感谢同事的分享让我更加明白单元测试的必要性。
非单元测试
在开始单元测试前,我们先来看看不属于单元测试的内容
新建一个SpringBoot模板工程,可以看到在工程结构中src源代码目录下有两个模块,一个是main模块用来编写和存放业务代码,另一个是test模块,用来对业务功能进行测试,在test模块的文件层级下可以看到SpringBoot已经默认为我们初始化了一个测试模板:
1 | class) (SpringRunner:: |
@RunWith(SpringRunner::class)
和@SpringBootTest
注解将会在测试运行时加载和启动Spring的上下文测试环境。
单元测试
下面开始进行单元测试的内容,我使用Kotlin+Gradle的方式创建了一个Spring Boot的项目
在Java开发中单元测试的工具有很多比如Junit,这里我使用的是TestNG,他继承了Junit的特性并且简洁对代码无污染,且功能强大。
引入TestNG相关的依赖包
1 | // testng依赖 |
比如在UserController中有个用户登录的login方法:
1 |
|
生成单元测试代码
我们要对UserController的login方法进行单元测试,我使用的开发工具是IDEA,在Mac上command+shift+t
, 如果是Windows则按controller+shift+t
,选择Create New Test,选择要创建单元测试的方法进行快速生成。
勾选 SetUp/@Before
和 tearDown/@After
,生成如下单元测试代码
1 | import org.testng.annotations.Test |
由于我们只对UserController类的login方法进行单元测试,该方法所依赖的Service等对象的方法和行为我们都需要进行Mock。这里暂且不讨论代码写的如何,也不必关心login方法中所调用的Service层的具体实现,login只需要关系自己的逻辑即可。
在login方法中调用了userService.getUser
来获取一个用户对象,通过判断用户是否存在进行下一步的逻辑验证,如果出现错误需要调用i18NService.getI18N
这个方法用来向用户返回对应的提示信息。这两处调用属于外部依赖的,无论实际业务代码是否实现这个功能,我们都不必关心,只需要Mock
模拟这写对象的调用行为即可。login只对自身方法的逻辑进行验证。
需要Mock的数据有:1
2
3
4userService.getUser(userLoginRequest.username, md5Pwd)
i18NService.getI18N("mer.coupon.accountError")
i18NService.getI18N("mer.coupon.userError")
i18NService.getI18N("mer.coupon.loginError")
使用when()
进行Mock数据
比如userService.getUser()
这个方法将会根据传入的参数去查询用户是否存在,如果存在则返回对象。这里不必关心具体的用户数据,只需要关心login()
自身的逻辑,所以我们对这个方法进行Mock即可,比如:1
`when`(userService.getUser(username, password)).thenReturn(response)
thenReturn(response)
中的response是我们自己构造的对象,when().thenReturn()
在实际单元测试时,将会模拟when()
中的代码行为,并且通过thenReturn()
返回调用该方法的返回值。这个返回值可以在单元测试中进行模拟。
单元测试一般要覆盖各种边界值,比如这里的login()
业务示例代码中,需要根据用户当前的启用和删除状态,以及所属商家的启用和删除状态进行登录限制。我们要模拟这些不同的数据进行输入,并且将输出的结果和预期的结果进行对比验证。
测试数据
测试数据这里用了一个Excel文件进行存放。对用户和所属商户的启用禁用的各种情况进行模拟不同的用户数据,第一行表头部分作为后续单元测试读取的键名称,expected
为这条数据登录后应该返回的期望状态,单元测试程序需要对比期望状态和实际调用login方法所返回的状态,如果相同则表明测试通过,如果不同则表明login()
方法有逻辑问题。
测试数据中包含了实际的业务场景,用户和所关联的商户必须都为未删除状态,且都必须是启用状态用户才能成功登陆。
编写单元测试
1 | package com.lanshiqin.boot |
@DataProvider(name="loginDataInfo")
所标注的方法表示该方法是一个数据提供者,指定名称为loginDataInfo
。@Test(dataProvider="loginDataInfo")
所标注的单元测试方法表示,该方法执行单元测试前将会先执行数据提供者loginDataInfo
所标注的方法,将数据提供者方法返回的数据作为本单元测试的入参。
when
使用引号引起来是因为单元测试的when
方法和kotlin的关键字when
冲突了,所以必须加上单引号
执行单元测试

根据测试数据中的边界值执行单元测试,每条数据所返回的结果与预期结果相同,测试通过!
后续如果不小心修改了UserController.login()
中的方法,比如不小心将业务代码中23行的的user.userDelete == 1
改成了 user.userDelete == 0
,这时候再运行一遍单元测试就会发现有一部分单元测试就无法通过了,通过预期值和实际值可以很容易的发现问题。
Mock
进行构造,并且应该尽可能的覆盖所有的边界情况,在特别复杂的业务代码中,单元测试显得至关重要。写完测试后,即使后续动到了核心的业务代码,只需要运行一遍单元测试就能判断逻辑是否有问题,而不是通过一遍一遍的断点调试,做重复的工作。