单元测试——编写手册
1.简述
本文主要针对如何使用Junit编写单元测试进行描述
文中的实例基于Junit 4
所谓单元测试,即是指针对程序中的一些单元进行测试的方法
这些单元在Junit中的最小单位为方法
借助单元测试,我们可以轻松地单独测试程序中的某一个逻辑片段而不需要在意程序的外部依赖和其它逻辑
接口测试 | 单元测试 |
---|---|
只能以接口为维度进行测试 | 只需被测试的单元逻辑正常即可 |
工程必须编译通过并打包进行部署 | 可以不依赖外部,测试进度不再受制于外部条件 |
工程的外部依赖(数据库、调用的服务等)必须就绪 | 可以以方法为维度进行测试 |
难以测试复杂的逻辑分支,为测试数据需要调整各个数据源(数据库、缓存、消息队列) | 可以根据单元的逻辑复杂程度编排测试用例数量,测试使用的数据可以自由调整 |
2.创建测试用例
2.1工程准备
-
确保工程的maven依赖中包含junit,版本至少为4.12,一般包含在spring-boot-starter-test中;
-
确保工程的目录中包含src/test/java和src/test/resource;
*部分工程没有src/test的相关包,需要手动创建
*src/test/java主要用于存放测试用例和测试相关的类
*src/test/resoutces主要用于存放测试使用的配置和资源文件
2.2 编写测试启动类
许多工程在启动时需要加载许多配置类,与外部系统进行连接,非常复杂
为了使单元测试更加轻便,我们可以编写单元测试专用的启动类,屏蔽一些不相关的启动项
若你的工程启动和外部连接依赖本身就很简单,可以省略这一步,不写测试启动类则执行测试时默认使用 src/main/java下的启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
@ComponentScan(basePackages = { "需要扫描的包名" }, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = { 扫描的包中需要排除的类 }) })
@SpringBootApplication(exclude = { 需要排除的一些启动时自动配置类 })
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
这个启动类中常用的配置如下
- @ComponentScan
用于扫描指定的包,excludeFilters用于排除扫描的包中不需要的Bean - @SpringBootApplication
用于排除一起自动配置的启动类,防止诸如数据库、Mongo等启动类进行外部连接
测试的启动类与工程的启动类类似,它通常防止在src/test/java下的项目根包中
其中“需要扫描的包名”和“扫描的包中需要排除的类”根据具体的工程决定,可按需将工程中的一些启动时配置类排除在外(Cache配置等),保证你测试的类以及它们的直接依赖被扫描到即可
“需要排除的一些启动时自动配置类”可参考如下对照表按需排除
作用 | 全类名 |
MongoDB自动配置 | org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration |
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration | |
数据源自动配置 | org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration |
Hibernate注解自动配置 | org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration |
启动类的扫描配置决定了测试用例的范围以及启动速度
可以按需编写多个不同扫描范围的启动类以适配不同的测试需求
2.3 构建抽象测试类
根据启动类创建一个抽象测试类是个好方法,它能够让你在创建测试用例时通过选择继承的父类直接确定使用的测试启动配置。
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = 启动类)
public abstract class BaseTest {
}
@SpringBootTest
用于指定测试的一些主要参数,主要是classes,用于指定关联的启动类,这样继承自这个类的所有子类都会使用相同的启动类
2.4 使用测试专用的配置
就像在工程部署时我们可以指定不同环境的配置文件一样;
执行测试时我们也可以使用不同的配置文件;
使用自定义测试启动类的情况下将配置文件放置在src/test/resource下则测试类默认会使用该包下的配置和资源文件;
由于测试启动类中通常会排除许多与测试目标无关的类,所以测试专用的配置文件可以更加轻量化,哪些被排除的类所引用的配置都可以省略;
2.5 新建Test类
针对一个被测类创建一个测试类
测试类通常以被测类的名字+Test进行命名,并放置在与src/test/java下与被测类同包名的包中
- 根据被测类创建测试类
- 配置测试类的参数,通常这些参数会按照约定给出一些默认值;
- 选择要测试的方法,注意:junit测试只能对非private方法进行测试DemoServiceTest,测试用例在src/test/java下的被测类同名包中;此时可以选择继承的父类,根据测试需要进行继承
- 生成测试类,得到一个单元测试类的骨架,包含了选择的方法的测试用例
编写用例
3.1 了解Mock测试
一个Bean通常会形成一个依赖树,这种发散的依赖结构导致我们在测试一个类的逻辑时其实是连同它所依赖的逻辑一同进行测试
此时使用Mock测试就可以解决依赖过多,逻辑复杂的问题
Mock以为模拟、虚拟,就是将原有的逻辑进行模拟,使用规划的Mock方案逻辑进行替代
在Mock测试中,我们通常对测试对象的所有直接依赖进行Mock,被Mock的直接依赖将变成只有方法签名的空壳
被调用时它们不会再调用间接依赖,也不会执行原有的逻辑,只会根据Mock方案进行返回
这样在测试测试对象时我们就不在需要关心它负载的间接依赖关系和所有依赖的内部逻辑了,只需要专注于当前测试对象的逻辑即可
3.2 完全Mock依赖
这种测试仅测试测试对象内部的逻辑,屏蔽所有测试对象的依赖
适用于测试内部逻辑较为复杂的对象,或依赖较为复杂的对象
样例:
注意
- @MockBean标记的依赖的方法默认变为空方法且返回值为null,一定要配合mock方案才能执行逻辑和获取返回值(参照FAQ-@MockBean与@SpyBean)
- mock方案的编写参照FAQ-mock方案
- 需要调用@MockBean标记的类中无返回值的方法时可以不编写mock方案,但一定要校验调用是否执行(参照FAQ-验证调用是否执行)
3.3 完全真实依赖
这种类型的测试同时测试测试对象及全部其依赖的逻辑,针对实际调用流程进行测试
适用于测试一个完整的流程以及测试调用链路正确性
样例
注意
- 这种测试方式与Mock毫无关系,为常规的方法逻辑调用
- 测试的结果与输入的数据、持久化的数据、程序逻辑直接相关,因此需要事先准备好配套得入参、数据库脚本/Redis缓存并熟悉程序的逻辑,以此推导出正确的处理结果
3.4 Mock与真实依赖相结合
这种类型的测试同时测试测试对象及其部分依赖的逻辑,屏蔽部分较为复杂的依赖同时对实际调用流程进行测试
适用于测试一个部分依赖较为复杂的流程以及测试调用链路正确性
样例:
注意
- @SpyBean标记的依赖的方法默认为真实逻辑,若配合mock方案则在入参与mock方案一致时根据mock方案的设置进行返回(参照FAQ-@MockBean与@SpyBean)
- mock方案的编写参照FAQ-mock方案
- 需要调用@SpyBean标记的类的方法的真实逻辑时可以不编写mock方案
- 测试的结果与输入的数据、持久化的数据、程序逻辑、mock方案均相关,因此需要事先准备好配套得入参、数据库脚本/Redis缓存并熟悉程序的逻辑和mock方案的逻辑,以此推导出正确的处理结果
- 虽然@SpyBean标记的类在没有设置mock方案的情况下原则上与原类的逻辑一致,但由于底层实现原理的种种限制,它与原生的类在某些情况下不能完全一致;若想直接测试完全真实依赖的场景,请参照完全使用真实依赖的测试章节的描述
3.5 混合测试
对一个测试对象中的不同方法采取不同的测试策略
对同一个测试对象的不同方法根据需求采取“完全Mock”或“完全真实”或“Mock与真实结合”的方式;没有对同一个测试 对象编写多个不同类型测试用例的需求
注意
- 此种测试是其他3种的复合形式,单个用例内的书写参照相应章节的描述
- 此种测试的测试对象的依赖需要使用@SpyBean的方式进行标记,否则难以执行真实逻辑(参照FAQ-@MockBean与@SpyBean)
4. FAQ
4.1 @MockBean与@SpyBean
在进行mock测试或混合测试时可以看到这2中不同的设定测试对象直接依赖的注解
/ | 相同 | 不同 |
---|---|---|
@MockBean | 提供mock测试能力mock方案的作用域都在一个@Test内 | 默认将标记该注解的类及其依赖的方法全部掏空,直接调用将没有任何执行逻辑并返回null(如果有返回值) |
@SpyBean | 提供mock测试能力mock方案的作用域都在一个@Test内 | 默认将标记该注解的类以及依赖的方法保持原样,直接调用将按照方法原本的逻辑执行并返回;可以按照混合测试的需求灵活决定是否mock |
/ | 使用场景 |
---|---|
@MockBean | 适用于标记的依赖需要被完全mock的场景,不配合mock方案使用易引发空指针异常 |
@SpyBean | 适用于标记的依赖只有部分逻辑需要mock的场景,不配合mock方案则执行原方法,配合mock方案则执行mock方案 |
4.2 mock方案
在进行mock测试时需要提前针对被mock的逻辑进行规划
参照如下写法:
Mockito.doReturn(mockData).when(demoService).function(Matchers.eq(arg1), Matchers.eq(arg2));
Mockito.doNothing().when(demoService).function(Matchers.eq(arg1), Matchers.eq(arg2));
Mockito.doThrow(exception).when(demoService).function((Matchers.eq(arg1), Matchers.eq(arg2));
关键词 | 解释 |
---|---|
doReturn(mockData) | 模拟方法正常返回数据 |
doNothing() | 模拟方法未执行(可用于没有返回值的方法) |
doThrow(exception) | 模拟方法执行时抛出异常 |
demoService | 被@MockBean或@SpyBean标记的测试对象的直接依赖 |
function、arg1、arg2 | 直接依赖的方法名及其参数 |
Matchers.eq() | org.mockito.Matchers提供的一些更灵活的mock调用时参数验证方法;验证通过时方法返回模拟的返回数据,验证不通过时方法按照@MockBean或@SpyBean的默认策略执行 |
4.3 同名方法调用多次
同名方法设置多个mock方案时,不同入参预期的方案都会保留,相同入参预期的方案以最后一个为准
例,设置了如下mock方案:
@MockBean
private DemoService demoService;
@Test
public void testFunction() {
// 省略其他步骤,此处只展示mock方案设置
// 方案1
Mockito.doReturn(demoDataA).when(demoService).callFun(eq("alpha"));
// 方案2
Mockito.doReturn(demoDataB).when(demoService).callFun(eq("beta"));
// 方案3
Mockito.doCallRealMethod().when(demoService).callFun(eq("beta"));
}
方案1,3会生效,方案2会被方案3覆盖而失效
调用demoService.callFun(String)方法时:
若入参为"alpha",则返回demoDataA;
若入参为"beta",则执行该方法的真实逻辑并返回;
若入参为其他,则执行空方法并返回null(@MockBean的默认方案);
在上述场景的基础上,若被测方法中存在同一方法调用多次且入参相同,但根据执行顺序返回值不同,则参照如下写法(使用链式调用依序排布多个方案):
@MockBean
private DemoService demoService;
@Test
public void testFunction() {
// 省略其他步骤,此处只展示mock方案设置
// 方案1
Mockito.doReturn(demoDataA).when(demoService).callFun(eq("alpha"));
// 方案2(包含多次调用的方案)
Mockito.doReturn(demoDataB).doCallRealMethod().when(demoService).callFun(eq("beta"));
}
调用demoService.callFun(String)方法时:
若入参为"alpha",则返回demoDataA;
若入参为"beta"且是该入参的第1次调用,则返回demoDataB;
若入参为"beta"且是该入参的第2次调用,执行该方法的真实逻辑并返回;
若入参为"beta"且是该入参的第3次及以上调用,执行方案链中最后一个方案(例子中为调用真实逻辑);
若入参为其他,则执行空方法并返回null(@MockBean的默认方案)
4.4 验证调用是否执行
在mock测试中需要验证mock方案标记的方法是否按照预期的入参执行过,若缺少这一步可能导致测试用例出现未预期的成功
例,一个用例按如下步骤执行:
- 测试的依赖使用@SpyBean标注
- 执行到mock方案对应的方法,入参与mock方案预期不一致
- 方法按照真实逻辑执行
- 方法的返回值恰巧与预期一致
- 测试成功
上述例子的用例可能会在某次数据库数据变动,外部链接中断的场景下执行失败
增加验证调用是否执行的步骤可以解决该问题
写法如下:文章来源:https://www.toymoban.com/news/detail-407042.html
Mockito.verify(demoService).function(Matchers.eq(arg1), Matchers.eq(arg2));
与mock方案中的写法非常相似
当同名方法调用多次且入参一致时采用如下写法:文章来源地址https://www.toymoban.com/news/detail-407042.html
Mockito.verify(demoService, new Times(2)).function(Matchers.eq(arg1), Matchers.eq(arg2));
到了这里,关于测试用例-单元测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!