Java单元测试实践-00.目录(9万多字文档+700多测试示例)
1. 单元测试Mock代码编写建议
- 按场景编写单元测试
在编写单元测试代码前,建议先详细整理对应交易的场景,可以使用思维导图或其他方式,再以此为基础,选择需要编写单元测试的场景,尽量覆盖全部的场景。
- 像业务代码一样编写单元测试代码
在编写单元测试代码时,应该像业务代码一样,对Mock代码进行封装,合理使用继承与多态,划分功能模块,减少重复代码,便于后续维护。可参考示例adrninistrator.test.testmock.example包中的代码。
- Stub条件应尽量精确
在对方法设置Stub时,Stub条件应尽量精确,避免使用宽泛的条件,以免对调用该方法的其他场景造成影响。
例如当某方法在单元测试过程中会被多次调用且参数不同时,Stub条件应避免使用Mockito.any()/any…()等方法,应指定具体的值。
- 合理选择Mock范围
应当合理选择Mock操作,尽量克制,使Mock范围最小化,只对有需要的方法进行Mock,防止扩大Mock范围后导致需要测试的代码未被测试,影响单元测试覆盖范围。
- 被Mock的方法应单独测试
在进行单元测试时,被Mock方法的真实方法不会执行,需要检查是否需要对真实方法单独进行测试,避免出现遗漏。
例如当需要从数据库或文件读取数据时,若将读取方法Mock,会导致读取方法未被单元测试覆盖,需要为对应的读取方法进行单元测试。
- 在测试基类打印类名
建议在测试基类中打印当前类名。当同时执行多个单元测试类时,便于通过单元测试输出的日志查找对应的类。
2. 单元测试Mock相关总结
2.1. 对Spring的@Component组件进行部分Mock
在进行单元测试时,有时需要对Spring的@Component组件的代码进行部分Mock,即部分代码进行Mock,其他代码执行真实方法。例如对远程服务调用的代码进行Mock,其他代码均执行真实方法,测试远程服务调用返回不同的值时,程序处理是否符合预期。
当需要对Spring的@Component组件的代码进行部分Mock时,可将需要Mock的代码相关的成员变量替换为Mock/Spy产生的成员变量,可以根据需要,使成员变量执行Mock后的方法,或真实方法。详细内容可参考前文。
2.2. Mock相关方法分类
- Mockito/PowerMockito.mock()
使用Mockito/PowerMockito.mock()创建Mock对象时,可以设置默认Answer,通过默认Answer设置Mock对象的操作,例如返回默认值、抛出异常、执行真实方法等。
- Mockito.mock(…class, AdditionalAnswers.delegatesTo())
使用Mockito.mock(…class, AdditionalAnswers.delegatesTo()),支持对指定Mock对象的非静态方法进行委托。当需要对被Mock的类进行完全重写时,适合使用该方法。
- Mockito/PowerMockito.when().then…()/do…().when()
使用Mockito/PowerMockito.when().then…()/do…().when(),支持对静态方法,或指定的Mock/Spy对象的非静态方法进行Stub,支持指定Stub条件,可以防止执行被Stub的真实方法(生成Mock对象时使用执行真实方法的默认Answer,或Spy对象),或抛出异常(生成Mock对象时使用抛出异常的默认Answer)。
- PowerMockito.stub()/replace()/suppress()
PowerMockito.stub()/replace()/suppress(),支持对指定类的静态方法或指定类的全部实例的非静态方法进行处理;suppress()方法支持对字段或方法进行禁止操作。stub()/replace()方法不支持指定生效条件,replace()方法可在执行时根据参数决定实际操作,但使用比较繁琐。
- @MockPolicy
与PowerMockito.stub()/replace()/suppress()功能类似。
使用@PrepareForTest注解后会导致@MockPolicy注解失效,可使用示例中提供的@TestInitAnnotation注解,与@MockPolicy注解功能类似,但不会因@PrepareForTest注解而失效。
2.3. Mock相关方法对比
对比内容 | Mockito/PowerMockito.when()/do…().when() | PowerMockito.stub()/replace() |
Stub操作需要指定的方法 | 静态方法:原始类的方法 非静态方法:Mock/Spy对象的方法 | 原始类的方法 |
Stub操作指定方法的方式 | 指定需要Stub的方法,当原始方法修改名称或参数时能够感知 | 指定需要Stub的方法名称,当原始方法修改名称或参数时无法感知 |
对非静态方法Stub的生效范围 | 指定的Mock/Spy对象的对应方法 | 指定类的全部实例的对应方法 |
对成员变量方法进行Stub的处理 | 将成员变量替换为方法被Stub的Mock/Spy对象,处理较复杂 | 直接对指定类的方法进行Stub,处理简单 |
Stub生效条件 | 在进行Stub时可以指定简单灵活的生效条件,不符合条件的方法调用不会被处理 | 不支持指定生效条件,方法调用都会被处理 replace()方法支持根据调用参数选择操作,但处理较繁琐 |
未Stub方法的默认行为 | Mock对象:默认返回默认值 Spy对象:执行真实方法 (调用参数不满足条件时情况相同) | 执行真实方法 |
适用场景 | 调用参数数量多、情况复杂的方法 | 调用参数数量少、情况简单的方法 |
以上所述“感知”,是指当原始方法修改名称或参数(数量、类型)时,编译阶段会报错,相关问题会很快暴露,并确认影响范围。
2.4. 不同情况可用的Mock方法对比
以下所述可用的Mock方法,当存在相似的方法时,仅说明使用最方便、容易维护的一种,例如当Mockito.when().thenReturn()与PowerMockito.when().thenReturn()均支持时,仅说明前者;当Mockito.when().thenReturn()与Mockito.doReturn().when()均支持时,仅说明前者;当Mockito.doReturn().when()与PowerMockito.when().thenReturn()均支持时,仅说明前者。
2.4.1. 静态方法
2.4.1.1. 静态公有非void方法
操作类型 | 可用方法 |
修改方法返回值 | Mockito.when().thenReturn() Mockito.when().thenAnswer() PowerMockito.stub().toReturn() PowerMockito.replace() @MockPolicy |
在方法中抛出异常 | Mockito.when().thenThrow() Mockito.when().thenAnswer() PowerMockito.stub().toThrow() PowerMockito.replace() @MockPolicy |
执行真实方法 | Mockito.when().thenCallRealMethod() Mockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
检查方法是否执行/调用参数 | Mockito.verify() Mockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
根据方法调用参数选择操作 | Mockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
禁止方法执行 | PowerMockito.suppress() @MockPolicy |
2.4.1.2. 静态公有void方法/私有方法
操作类型 | 可用方法 |
修改方法返回值(私有非void方法) | PowerMockito.when().thenReturn() PowerMockito.when().thenAnswer() PowerMockito.stub().toReturn() PowerMockito.replace() @MockPolicy |
在方法中抛出异常 | PowerMockito.when().thenThrow() PowerMockito.when().thenAnswer() PowerMockito.stub().toThrow() PowerMockito.replace() @MockPolicy |
执行真实方法 | PowerMockito.when().thenCallRealMethod() PowerMockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
检查方法是否执行/调用参数 | Mockito.verify() PowerMockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
根据方法调用参数选择操作 | PowerMockito.when().thenAnswer() PowerMockito.replace() @MockPolicy |
禁止方法执行 | PowerMockito.suppress() @MockPolicy |
2.4.1.3. 静态代码块
禁止静态代码块执行,可使用@SuppressStaticInitializationFor注解。
2.4.2. 所有实例的非静态方法
当需要使Mock对某个类的所有实例的非静态方法均生效时,可以使用以下方法。
操作类型 | 可用方法 |
修改方法返回值 | PowerMockito.stub().toReturn() PowerMockito.replace() @MockPolicy |
在方法中抛出异常 | PowerMockito.stub().toThrow() PowerMockito.replace() @MockPolicy |
执行真实方法 | PowerMockito.replace() @MockPolicy |
检查方法是否执行/调用参数 | PowerMockito.replace() @MockPolicy |
根据方法调用参数选择操作 | PowerMockito.replace() @MockPolicy |
禁止方法执行 | PowerMockito.suppress() @MockPolicy |
2.4.3. 指定的Mock对象非静态方法
当需要使Mock仅对某个类的指定的Mock对象(通过Mockito.mock()等方法生成)的非静态方法生效时,可以使用以下方法。
2.4.3.1. Mock对象非静态公有非void方法
操作类型 | 可用方法 |
修改方法返回值 | Mockito.when().thenReturn() Mockito.when().thenAnswer() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
在方法中抛出异常 | Mockito.when().thenThrow() Mockito.when().thenAnswer() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
执行真实方法 | Mockito.when().thenCallRealMethod() Mockito.when().thenAnswer() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
检查方法是否执行/调用参数 | Mockito.verify() PowerMockito.when().thenAnswer() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
根据方法调用参数选择操作 | PowerMockito.when().thenAnswer() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
2.4.3.2. Mock对象非静态公有void方法
操作类型 | 可用方法 |
在方法中抛出异常 | Mockito.doThrow().when() Mockito.doAnswer().when() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
执行真实方法 | Mockito.doCallRealMethod().when() Mockito.doAnswer().when() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
检查方法是否执行/调用参数 | Mockito.verify() Mockito.doAnswer().when() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
根据方法调用参数选择操作 | Mockito.doAnswer().when() Mockito.mock(…, AdditionalAnswers.delegatesTo()) |
2.4.3.3. Mock对象非静态私有方法
操作类型 | 可用方法 |
修改方法返回值 | PowerMockito.doReturn().when() PowerMockito.doAnswer().when() |
在方法中抛出异常 | PowerMockito.doThrow().when() PowerMockito.doAnswer().when() |
执行真实方法 | PowerMockito.doCallRealMethod().when() PowerMockito.doAnswer().when() |
检查方法是否执行/调用参数 | PowerMockito.verifyPrivate() PowerMockito.doAnswer().when() |
根据方法调用参数选择操作 | PowerMockito.doAnswer().when() |
2.4.4. 指定的Spy对象非静态方法
当需要使Mock仅对某个类的指定的Spy对象(通过Mockito.spy()等方法生成)的非静态方法生效时,可以使用以下方法。
2.4.4.1. Spy对象非静态公有方法
操作类型 | 可用方法 |
修改方法返回值 | Mockito.doReturn().when() Mockito.doAnswer().when() |
在方法中抛出异常 | Mockito.doThrow().when() Mockito.doAnswer().when() |
执行真实方法 | Mockito.doCallRealMethod().when() Mockito.doAnswer().when() |
检查方法是否执行/调用参数 | Mockito.verify() Mockito.doAnswer().when() |
根据方法调用参数选择操作 | Mockito.doAnswer().when() |
2.4.4.2. Spy对象非静态私有方法
与指定的Mock对象非静态私有方法处理相同。
2.5. 常见Mock场景总结
以下对常见的需要进行Mock的场景进行总结。
2.5.1. Mock远程服务调用
在进行单元测试的环境,远程服务很可能无法正常访问。可以通过Mock修改远程服务调用方法,使远程服务调用不被发送到实际的服务器,避免出现异常;且可以修改返回值,验证不同场景下远程服务调用返回不同值时,程序处理是否符合预期。
通过Mock修改远程服务调用方法,可以对请求参数进行检查或打印,验证调用远程服务发送的请求参数是否正确。
有时需要验证远程服务调用超时的情况下,程序处理是否正常,可以通过Mock修改远程服务调用方法,延长调用耗时。
对远程服务调用进行Mock时,适合使用Mockito的Answer进行回调处理,可以修改返回值,获取方法调用次数,获取请求参数信息。
Mock静态方法形式的远程服务调用,可参考TestRpcCallStatic类,Mock Spring服务形式的远程服务调用方法,可参考TestRpcCallService类。
以上所述示例Answer如下所示,在调用构造函数时可以指定方法被修改的返回值,调用getReqList()方法可以获得调用远程服务的次数及参数列表:
// 被Stub的方法
String rpcCall(String serviceInfo, String req);
// Answer实现类
public class AnswerRpcCall implements Answer {
private static final Logger logger = LoggerFactory.getLogger(AnswerRpcCall.class);
// 保存请求内容的列表
private List<String> reqList = new ArrayList<>();
// Mock后的返回
private String mockedRsp;
public AnswerRpcCall(String mockedRsp) {
this.mockedRsp = mockedRsp;
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
String serviceInfo = invocation.getArgument(0);
String req = invocation.getArgument(1);
logger.info("### serviceInfo: {} req: {}", serviceInfo, req);
reqList.add(req);
// 使用Mock后的返回
return mockedRsp;
}
public List<String> getReqList() {
return reqList;
}
}
2.5.2. 从数据库或文件读取数据
对于需要从数据库或文件读取数据的情况,在进行单元测试时,若没有在单元测试中提前插入对应的数据,则需要对读取数据的方法进行Mock,返回所需的数据。
2.5.3. 跳过检查操作
在单元测试中,可以通过Mock将一些需要跳过检查操作修改为返回固定值,使交易能够继续执行。
例如密码验证、安全检查等操作。
2.5.4. 跳过AOP处理
通过Mock可以跳过AOP的处理,直接执行对应的原始代码。
例如通过AOP实现的对登录用户的会话、权限检查等,可以使用Mock跳过。
2.5.5. 检查特定方法是否执行/调用参数
通过Mock可以检查特定方法是否执行,以及调用参数是否符合预期。
例如检查是否有通过邮件/短信等发送通知,请求参数是否满足要求等。
2.5.6. 禁止特定方法执行
通过Mock可以禁止特定方法执行,防止出现异常或对单元测试产生影响。
例如禁止第三方Jar包中的初始化方法,禁止定时任务等。
2.6. 使用Mockito、PowerMock容易出现的问题总结
- Mockito.any…()不匹配null
对于Mockito 2及以上版本,当Stub条件使用Mockito.any…()时,例如Mockito.anyString()等,不能匹配null。
若对某方法进行Stub,某个参数使用Mockito.any…()作为条件,当调用该方法的参数为null时,Stub不会生效。
例如存在方法C1.f1(String str),对其进行Stub“Mockito.when(C1.f1(Mockito.anyString())).thenThen(…)”,若执行T.f1(null),则Stub不生效。
Mockito.any()可以匹配null。
- 被Mock类的未Stub方法返回值
当对某个类使用PowerMockito.mockStatic()方法进行Mock时,对于该类未被Stub的静态方法,在执行时受到Mock时指定的默认Answer控制,若在执行PowerMockito.mockStatic()方法时未指定默认Answer,则会使用返回默认值的Answer。
例如对类C1执行PowerMockito.mockStatic(C1.class),则未被Stub的静态方法都会返回默认值(基本类型默认值或null等)。
类C1中包含方法f1()、f2()等,对f1()方法进行了Stub,在执行f2()方法时,会返回默认值,而不会执行真实方法。
若希望在执行C1.f2()方法时能够执行真实方法,则需要对f2()方法执行Stub,使其执行真实方法;或在执行PowerMockito.mockStatic()方法时,设置默认Answer执行真实方法,示例如下:
Mockito.when(C1.f2(Mockito.anyString())).thenCallRealMethod();
PowerMockito.mockStatic(C1.class, new CallsRealMethods());
- PowerMockito.mockStatic()执行多次
每次在对某个类执行PowerMockito.mockStatic()方法时,该类的静态方法在执行时均会受到Mock时指定的默认Answer控制,若之前有方法已设置Stub,则会失效。
若在进行测试时不希望针对某静态方法的Stub失效,则需要注意执行PowerMockito.mockStatic()方法的时机,避免导致Stub失效。
- Stub条件固定值不能与Mockito类的方法混用
在对Mock/Spy对象进行Stub时,Stub条件中不能出现固定值与Mockito类提供的相关方法混用。
例如以下指定的Stub条件会出现异常:
Mockito.when(C1.f1("...", Mockito.anyString())).thenCallRealMethod();
若Stub条件中确实需要使用固定值及Mockito类提供的相关方法,可使用Mockito.eq()指定Stub条件为需要等于指定值,如下所示:
Mockito.when(C1.f1(Mockito.eq("..."), Mockito.anyString())).thenCallRealMethod();
- Mock时指定默认Answer执行真实方法,执行Stub时执行真实方法
当对某个类或对象执行Mock时,指定默认Answer执行真实方法,在对该类或对象的方法执行Stub时,若使用Mockito.when().then…()方式,会执行真实方法(对于接口的Mock对象,上述情况不会执行真实方法;实现类的Mock对象,会执行真实方法)。
例如以下示例中,C1.f1()与c1.f1()方法均会执行真实方法:
PowerMockito.mockStatic(C1.class, new CallsRealMethods());
Mockito.when(C1.f1(Mockito.anyString())).thenReturn("");
C1 c1= Mockito.mock(C1.class, new CallsRealMethods());
Mockito.when(c1.f1(Mockito.anyString())).thenReturn("");
为了避免以上情况执行Stub时执行真实方法,对于静态方法,在Stub时可以使用PowerMockito.do…().when()方法;对于非静态方法,在Stub时可以使用Mockito.do…().when()方法,如下所示:
PowerMockito.doReturn("").when(C1.class, "f1", Mockito.anyString());
Mockito.doReturn("").when(c1).test1(Mockito.anyString());
- Mock时指定默认Answer抛出异常,执行Stub时抛出异常
当对某个类或对象执行Mock时,指定默认Answer抛出异常,在对该类或对象的方法执行Stub时,若使用Mockito.when().then…()方式,会抛出异常。
解决方法同上。
- 对Spy对象执行Stub时执行真实方法
在对Spy对象进行Stub时,若使用Mockito.when().then…()方式,会执行真实方法,应使用Mockito.do…().when()方法。
- 执行被Stub方法时,参数不满足Stub条件
对于被Mock的类或对象,在执行被Stub的方法时,若参数不满足Stub条件,则该方法的行为受到默认Answer的控制。
对于Spy对象,在执行被Stub的方法时,若参数不满足Stub条件,则会执行真实方法。
- 调用Spring @Component组件的Mock对象的真实方法
对于Spring @Component组件的接口的Mock对象,对其方法进行Stub,设置执行真实方法,在执行对应方法时,不会执行真实方法;
对于Spring @Component组件的实现类的Mock对象,对其方法进行Stub,设置执行真实方法,在执行对应方法时,会执行真实方法,但由于未完成依赖注入,引用的成员变量为空,可能会出现异常。
为了使Spring @Component组件能够根据需要执行真实方法,或执行Stub指定的操作,应当使用Spy对象,而不是Mock对象。
- 对被AOP处理的类进行Mock
对于被AOP处理的类,通过@Autowired注解等方式注入其对象时,获取到的对象为代理对象。
对于代理对象,无法直接进行Mock或Spy等操作,需要使用AopTestUtils.getTargetObject()等方法获得代理对象对应的原始对象,对原始对象进行Mock、Spy、Stub等操作后,可将注入了代理对象的类使用原始对象的Mock/Spy对象进行替换,Mock相关操作可以生效。
对于使用了@Transactional、@Async等注解的类,也会被处理为代理形式,处理方法相同。
- 对Mybatis的Mapper对象进行Mock
对于Mybatis的Mapper对象,如果需要获得Mock对象并执行Stub,与操作普通的对象没有区别。
Mybatis的Mapper对象不支持执行Spy操作,如果需要使其根据需要执行真实方法或执行Stub指定的操作,可使用以下方法:
- 获得Mapper对象中的MapperProxy对象,对MapperProxy对象进行Spy、Stub操作后,将MapperProxy对象的Spy对象替换到Mapper对象中;
- 对MapperProxy类的invoke()方法进行Replace;
- 将Mapper对象替换为Mapper对象的Mock对象,对Mock对象进行Stub,根据需要执行Stub指定的操作,或调用原始Mapper对象,执行真实方法。