Java单元测试实践-14.Mock、Spy后Stub Spring成员变量中的方法

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

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的方法,可以按照需求改变其行为。

示意图如下所示:

pic

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对象后,所有注入该对象的其他原始对象均会受影响。

示意图如下所示:

pic

示例如下:

测试代码中引用了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.lang.AssertionError: Expecting code to raise a throwable. at com.htsc.service.FundCostStatisticsV2ServiceTest.testGetFundCost_JdbcTemplateThrowsDataAccessException(FundCostStatisticsV2ServiceTest.java:858) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37) at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:231) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
最新发布
10-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值