Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.youkuaiyun.com/a82514921/article/details/107969340
1. Mock/Spy后Stub Spring成员变量中的方法
在被测试代码中,Spring的@Component组件实现类中通常会依赖其他的成员变量,可能存在多层的依赖关系。为了使被测试代码能够完成足够完备的测试,覆盖足够的场景与代码分支,需要对被测试代码进行细粒度的控制,可能需要对成员变量的方法进行Stub。
例如被测试代码中调用了A类的对象a1的fa1()方法,在a1.fa1()方法中,调用了B类的对象b1的fb1()方法,为了使a1.fa1()方法中调用的b1.fb1()方法的行为被Stub,可将a1对象中注入的b1对象替换为B类的Mock或Spy对象,并对其fb1()方法进行Stub。通过以上操作后,在调用a1.fa1()方法时,会调用B类的Mock/Spy对象被Stub的方法,可以按照需求改变其行为。
示意图如下所示:

1.1. 使用Mock对象对成员变量进行替换
1.1.1. 替换成员变量为Mock对象
使用Whitebox.setInternalState()方法可以通过反射对私有的成员变量进行替换,可将Spring的@Component组件实现类中的成员变量替换为Mock对象,可对Mock对象的方法进行Stub,在被测试类调用成员变量的方法时按照需要改变行为。
示例如下:
被测试类为TestServiceB1Impl,在其中引用了TestServiceA1Impl类的实例,TestServiceB1Impl类的test1方法调用了TestServiceA1Impl类的test1方法。
生成TestServiceA1接口的Mock对象,对其test1方法进行Stub,并将TestServiceB1Impl实例中的TestServiceA1Impl类的实例替换为Mock对象。
当调用TestServiceB1Impl类的test1方法时,会调用TestServiceA1Impl类的Mock对象被Stub的test1方法。可参考示例TestSpMockMember类。
@Autowired
private TestServiceB1 testServiceB1;
TestServiceA1 testServiceA1 = Mockito.mock(TestServiceA1.class);
Mockito.when(testServiceA1.test1(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
Whitebox.setInternalState(testServiceB1, testServiceA1);
1.1.2. 替换成员变量的成员变量为Mock对象
有时需要将被测试类的成员变量的成员变量替换为Mock对象,可以使用反射获取被测试类的成员变量( 可使用Whitebox.getInternalState()方法 ),再将成员变量的成员变量替换为Mock对象,并对其方法进行Stub。
示例如下:
被测试类为TestServiceC1Impl,在其中引用了TestServiceB1Impl类的实例,TestServiceB1Impl类中引用了TestServiceA1Impl类的实例。
TestServiceC1Impl类的test1()方法调用了TestServiceB1Impl类的test1()方法,在其中调用了TestServiceA1Impl类的test1()方法。
生成TestServiceA1接口的Mock对象,对其test1()方法进行Stub。通过反射获取TestServiceC1Impl类实例中的TestServiceB1Impl类的实例,将其中的TestServiceA1Impl类的实例替换为Mock对象。
当调用TestServiceC1Impl类的test1()方法时,会调用TestServiceB1Impl类的test1()方法,再调用TestServiceA1Impl类的Mock对象被Stub的test1()方法。可参考示例TestSpMockMemberOfM1类。
@Autowired
private TestServiceC1 testServiceC1;
TestServiceA1 testServiceA1 = Mockito.mock(TestServiceA1.class);
Mockito.when(testServiceA1.test1(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
TestServiceB1 testServiceB1 = Whitebox.getInternalState(testServiceC1, TestServiceB1.class);
Whitebox.setInternalState(testServiceB1, testServiceA1);
1.1.3. Spring Bean单例与变量替换
由于Spring的Bean默认为单例的,将某个@Component组件对象中的成员变量替换为Mock/Spy对象后,所有注入该对象的其他原始对象均会受影响。
示意图如下所示:

示例如下:
测试代码中引用了TestServiceC1Impl与TestServiceC2Impl类的实例,以上两个类均引用了TestServiceB1Impl类的实例,TestServiceB1Impl类中引用了TestServiceA1Impl类的实例。
TestServiceC1Impl与TestServiceC2Impl类的test1()方法调用了TestServiceB1Impl类的test1()方法,在其中调用了TestServiceA1Impl类的test1()方法。
生成TestServiceA1接口的Mock对象,对其test1()方法进行Stub。通过反射获取TestServiceC1Impl类实例中的TestServiceB1Impl类的实例,将其中的TestServiceA1Impl类的实例替换为Mock对象。
当调用TestServiceC1Impl或TestServiceC2Impl类的test1()方法时,会调用TestServiceB1Impl类的test1()方法,再调用TestServiceA1Impl类的Mock对象被Stub的test1()方法,因此TestServiceC1Impl与TestServiceC2Impl类的test1()方法的行为均被改变了。可参考示例TestSpMockMemberOfM2类。
@Autowired
private TestServiceC1 testServiceC1;
@Autowired
private TestServiceC2 testServiceC2;
TestServiceA1 testServiceA1 = Mockito.mock(TestServiceA1.class);
Mockito.when(testServiceA1.test1(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
TestServiceB1 testServiceB1InC1 = Whitebox.getInternalState(testServiceC1, TestServiceB1.class);
Whitebox.setInternalState(testServiceB1InC1, testServiceA1);
1.1.4. 将多个类引用的实例替换为独立的Mock对象
有时可能出现多个不同的类引用了同一个类的实例,可能需要将不同类中的被引用类的实例替换为独立的Mock对象,使不同的类在调用被引用类时能够按照需要执行不同的操作。
示例如下:
测试代码中引用了TestServiceC1Impl与TestServiceC2Impl类的实例,以上两个类均引用了TestServiceB1Impl类的实例。
TestServiceC1Impl与TestServiceC2Impl类test1()方法均调用了TestServiceB1Impl类的test1()方法。
可以将TestServiceC1Impl与TestServiceC2Impl类的实例中的TestServiceB1Impl类的实例分别替换为独立的Mock对象,使以上两个类在调用TestServiceB1Impl类的test1()方法时能够按照需要分别进行Stub。可参考示例TestSpMockMemberOfM3类。
@Autowired
private TestServiceC1 testServiceC1;
@Autowired
private TestServiceC2 testServiceC2;
TestServiceB1 testServiceB1InC1 = Mockito.mock(TestServiceB1.class);
Mockito.when(testServiceB1InC1.test1(Mockito.anyString())).thenReturn(TestConstants.FLAG1);
Whitebox.setInternalState(testServiceC1, testServiceB1InC1);
TestServiceB1 testServiceB1InC2 = Mockito.mock(TestServiceB1.class);
Mockito.when(testServiceB1InC2.test1(Mockito.anyString())).thenReturn(TestConstants.FLAG2);
Whitebox.setInternalState(testServiceC2, testServiceB1InC2);
1.1.5. 替换成员变量时防止覆盖Stub操作
在将成员变量替换为Mock对象之前,需要先判断该成员变量目前是否已经是Mock对象,若已经是Mock对象,需要直接对该Mock对象进行Stub;若每次都生成新的Mock对象进行替换,会将之前的Mock对象的Stub操作覆盖。可以参考示例代码TestReplaceUtil类的replaceMockMember()/replaceSpyMember(),将指定对象中的指定类型成员变量替换为Mock/Spy对象,且可以防止之前设置的覆盖Stub操作。
- 示例如下
被测试类为TestServiceB1Impl,在其中引用了TestServiceA1Impl类的实例,test1()与test3()方法分别调用TestServiceA1Impl类的同名方法。
- 反例如下
在对TestServiceB1Impl类实例中的TestServiceA1Impl类实例的test1()方法进行Stub时,生成了TestServiceA1接口的Mock对象,对其test1()方法进行Stub,再将TestServiceB1Impl类实例中的TestServiceA1Impl类实例替换为Mock对象。
在对TestServiceB1Impl类实例中的TestServiceA1Impl类实例的test3()方法进行Stub时,生成了TestServiceA1接口的Mock对象,对其test3()方法进行Stub,再将TestServiceB1Impl类实例中的TestServiceA1Impl类实例替换为Mock对象。
以上操作导致TestServiceB1Impl类实例中的TestServiceA1Impl类实例的Mock对象被替换为最后一次生成的Mock对象,仅包含对test3()方法的Stub,对test1()方法的Stub已被覆盖。可参考示例TestSpMockMemberOfM4Run、TestSpMockMemberOfM4Mock类。
@Autowired
private TestServiceB1 testServiceB1;
TestServiceA1 testServiceA1Mock = Mockito.mock(TestServiceA1.class);
Mockito.when(testServiceA1Mock.test1(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
Whitebox.setInternalState(testServiceB1, testServiceA1Mock);
TestServiceA1 testServiceA1Mock2 = Mockito.mock(TestServiceA1.class);
Mockito.when(testServiceA1Mock2.test3(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
Whitebox.setInternalState(testServiceB1, testServiceA1Mock2);
- 正例如下
在对TestServiceB1Impl类实例中的TestServiceA1Impl类实例的test1()方法进行Stub时,判断TestServiceB1Impl类实例中的TestServiceA1Impl类实例是否为Mock对象,若非Mock对象,则生成Mock对象,对其test1()方法进行Stub后,再将TestServiceB1Impl类实例中的TestServiceA1Impl类实例替换为Mock对象;若为Mock对象,则直接对Mock对象进行Stub。
在对TestServiceB1Impl类实例中的TestServiceA1Impl类实例的test3()方法进行Stub时,判断TestServiceB1Impl类实例中的TestServiceA1Impl类实例是否为Mock对象,若非Mock对象,则生成Mock对象,对其test3()方法进行Stub后,再将TestServiceB1Impl类实例中的TestServiceA1Impl类实例替换为Mock对象;若为Mock对象,则直接对Mock对象进行Stub。
以上操作使TestServiceB1Impl类实例中的TestServiceA1Impl类实例的Mock对象仅生成一次,包含了每次的Stub操作,不会有Stub操作被覆盖。可参考示例TestSpMockMemberOfM5Run、TestSpMockMemberOfM5Mock方法。
public static void mock2(TestServiceB1 testServiceB1) {
TestServiceA1 testServiceA1InB1 = Whitebox.getInternalState(testServiceB1, TestServiceA1.class);
if (testServiceA1InB1.getClass() == TestServiceA1Impl.class) {
TestServiceA1 testServiceA1Mock = Mockito.mock(TestServiceA1.class);
doMock2(testServiceA1Mock);
Whitebox.setInternalState(testServiceB1, testServiceA1Mock);
} else {
doMock2(testServiceA1InB1);
}
}
private static void doMock2(TestServiceA1 testServiceA1) {
Mockito.when(testServiceA1.test3(Mockito.anyString())).thenReturn(TestConstants.MOCKED);
}
1.1.6. 变量替换与Stub的顺序
将变量替换为Mock对象,与对Mock对象的方法进行Stub的操作,以上两个操作的先后顺序不影响执行结果,可以任意指定。可参考示例TestSpMockMemberOrder类。
1.2. 使用Spy对象对成员变量进行替换
使用Spy对象对成员变量进行替换,与使用Mock对象对成员变量进行替换类似。
- 替换成员变量为Spy对象
可参考示例TestSpSpyMember类。
- 替换成员变量的成员变量为Spy对象
可参考示例TestSpSpyMemberOfM1类。
- Spring Bean单例与变量替换
可参考示例TestSpSpyMemberOfM2类。
- 将多个类引用的实例替换为独立的Spy对象
可参考示例TestSpSpyMemberOfM3类。
- 替换成员变量时防止覆盖Stub操作
反例可参考示例TestSpSpyMemberOfM4Run、TestSpSpyMemberOfM4Spy类,正例可参考TestSpSpyMemberOfM5Run、TestSpSpyMemberOfM5Spy类。
- 变量替换与Stub的顺序
可参考示例TestSpSpyMemberOrder类。

本文详细介绍如何在Java单元测试中使用Mockito对Spring组件中的多级依赖进行Mock和Spy,包括成员变量及其成员变量的替换,处理SpringBean单例特性,避免Stub操作覆盖,以及操作顺序的影响。
7647

被折叠的 条评论
为什么被折叠?



