天天看點

Java單元測試實踐-19.Mockito與PowerMock的其他功能1. Mockito與PowerMock的其他功能2. 使用Mock禁止Spring定時任務

Java單元測試實踐-00.目錄(9萬多字文檔+700多測試示例)

https://blog.csdn.net/a82514921/article/details/107969340

1. Mockito與PowerMock的其他功能

1.1. 擷取Mock對象詳細資訊

Mockito提供了用于擷取Mock對象詳細資訊的API,可參考 https://static.javadoc.io/org.mockito/mockito-core/latest/org/mockito/Mockito.html#mocking_details 。

1.1.1. 判斷指定對象是否為Mock/Spy對象

使用Mockito.mockingDetails()方法,可以擷取指定對象對應的MockingDetails執行個體,可以獲得Mock的相關資訊。

MockingDetails.isMock()、isSpy()方法分别可以判斷指定的對象是否為Mock對象或Spy對象。

使用MockingDetails.isMock()、isSpy()方法可以準确地判斷指定的對象是Mock對象、Spy對象還是原始對象,使用示例如下,可參考示例TestSpringMockDetailIs1類。

assertFalse(Mockito.mockingDetails(new TestServiceA1Impl()).isMock());
assertFalse(Mockito.mockingDetails(new TestServiceA1Impl()).isSpy());

assertTrue(Mockito.mockingDetails(Mockito.mock(TestServiceA1.class)).isMock());
assertTrue(Mockito.mockingDetails(Mockito.spy(TestServiceA1.class)).isSpy());
           

1.1.1.1. 将對象中的成員變量替換為Mock/Spy對象公共方法

在替換成員變量時,為了防止覆寫原有Stub操作,可以使用MockingDetails.isMock()、isSpy()方法來準确地判斷指定成員變量是否已經是Mock/Spy對象。

使用MockingDetails.isMock()、isSpy()方法判斷對象是否為Mock/Spy對象的功能,将替換成員變量的操作整理為公共方法,能夠實作将指定對象的指定類型成員變量替換為Mock/Spy對象,可以簡化操作,并避免将現有Mock/Spy對象的Stub操作覆寫。

在示例代碼中,提供了将成員變量替換為Mock/Spy對象,且可以避免覆寫原有Stub的簡化公共方法。可使用示例TestReplaceUtil類的replaceMockMember、replaceSpyMember方法。

示例如下,傳回的對象為testServiceB1對象中的TestServiceA1類的Mock/Spy對象:

TestServiceA1 testServiceA1InB1 = TestReplaceUtil.replaceMockMember(testServiceB1, TestServiceA1.class);

TestServiceA1 testServiceA1InB1 = TestReplaceUtil.replaceSpyMember(testServiceB1, TestServiceA1.class);
           

以上公共方法的使用可參考示例TestSpMockMemberOfMCommRun、TestSpMockMemberOfMCommMock、TestSpSpyMemberOfMCommRun、TestSpSpyMemberOfMCommSpy類。

1.1.2. 擷取Mock對象原始類型及預設Answer等詳細資訊

使用MockingDetails.getMockCreationSettings()方法,可以擷取對應的MockCreationSettings執行個體,可以獲得Mock在建立時的設定。

  • 擷取Mock對象原始類型

使用MockCreationSettings.getTypeToMock()方法可以擷取Mock對象原始類型,使用示例如下,可參考示例TestSpringMockDetailMock1類。

assertEquals(RuntimeException.class,
        Mockito.mockingDetails(Mockito.mock(RuntimeException.class)).getMockCreationSettings().getTypeToMock());
           
  • 擷取預設Answer

使用MockCreationSettings.getDefaultAnswer()方法可以擷取Mock對象的預設Answer,使用示例如下,可參考示例TestSpringMockDetailMock1類。

assertSame(Mockito.RETURNS_DEFAULTS,
        Mockito.mockingDetails(Mockito.mock(RuntimeException.class)).getMockCreationSettings().getDefaultAnswer());
           

擷取Spy對象的以上資訊可參考示例TestSpringMockDetailSpy1類。

1.1.3. 擷取Mock對象的Stub設定及方法調用情況

  • 擷取Mock對象的Stub設定

使用MockingDetails.getStubbings()方法,可以擷取指定Mock對象的Stub設定,類型為Collection<Stubbing>,Mock對象初始的Stub設定數量為0,每設定一個Stub條件,數量加1。

Stubbing對象的toString()方法會傳回Stub的詳細資訊。設定以下Stub條件後,通過MockingDetails.getStubbings()方法擷取Stubbing對象并列印,如下所示。可參考示例TestSpringMockDetailMock2類。

Mockito.when(testServiceA1Mock.test1(Mockito.anyString())).thenReturn(TestConstants.MOCKED);

Mockito.doAnswer(invocation -> null).when(testServiceA1Mock).test2(Mockito.any(StringBuilder.class));

Mockito.when(testServiceA1Mock.test3(Mockito.anyString())).thenThrow(new RuntimeException(TestConstants.MOCKED));
           
testServiceA1.test1(<any string>); stubbed with: [Returns: MOCKED]

testServiceA1.test2(
    <any java.lang.StringBuilder>
); stubbed with: [adrninistrator.test.testmock.detail.TestSpringMockDetailMock2$$Lambda$1/[email protected]]

testServiceA1.test3(<any string>); stubbed with: [[email protected]]
           
  • 擷取Mock對象的方法調用情況

使用MockingDetails.getInvocations()方法,可以擷取指定Mock對象的方法調用情況,類型為Collection<Invocation>,初始數量為0,每調用一次方法,數量加1。

Invocation對象的toString()方法會傳回Invocation的詳細資訊。調用以下方法後,通過MockingDetails.getInvocations()方法擷取Invocation對象并列印,如下所示:

testServiceA1Mock.test1(TestConstants.FLAG1);

testServiceA1Mock.test2(new StringBuilder().append(TestConstants.FLAG2));
           
testServiceA1.test1("flag1");

testServiceA1.test2(flag2);
           

MockingDetails.printInvocations(),以友好的方式展示指定Mock對象已發生的的調用資訊。此外,該方法還會列印Stub資訊,包括未使用的Stub。該方法傳回類型為String。以上Mock對象的MockingDetails.printInvocations()傳回結果如下所示。可參考示例TestSpringMockDetailMock2類。

[Mockito] Interactions of: Mock for TestServiceA1, hashCode: 800722348
 1. testServiceA1.test1("flag1");
  -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.test(TestSpringMockDetailMock2.java:51)
   - stubbed -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.init(TestSpringMockDetailMock2.java:32)
 2. testServiceA1.test2(flag2);
  -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.test(TestSpringMockDetailMock2.java:54)
   - stubbed -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.init(TestSpringMockDetailMock2.java:35)

[Mockito] Unused stubbings of: Mock for TestServiceA1, hashCode: 800722348
 1. testServiceA1.test1("");
  - stubbed -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.init(TestSpringMockDetailMock2.java:32)
 2. testServiceA1.test2(null);
  - stubbed -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.init(TestSpringMockDetailMock2.java:35)
 3. testServiceA1.test3("");
  - stubbed -> at adrninistrator.test.testmock.detail.TestSpringMockDetailMock2.init(TestSpringMockDetailMock2.java:38)
           

擷取Spy對象的以上資訊可參考示例TestSpringMockDetailSpy2類。

1.2. 使用@InjectMocks注解實作Mock/Spy對象的自動注入

使用@InjectMocks注解可以實作Mock/Spy對象的自動注入。例如使用@InjectMocks注解指定C類的執行個體,再使用@Mock/@Spy注解指定C類中包含的成員變量A、B類的執行個體,可以實作将A、B類的Mock/Spy對象自動注入至C類執行個體中。示例如下:

@InjectMocks
private C c;

@Spy
private B b = new B();

@Mock
private A a;
           

@InjectMocks注解需要指定實作類對象,不能指定接口對象,否則會出現類似以下異常:

org.mockito.exceptions.base.MockitoException: 
Cannot instantiate @InjectMocks field named 'testServiceC1'! Cause: the type 'TestServiceC1' is an interface.
You haven't provided the instance at field declaration so I tried to construct the instance.
           

使用@InjectMocks注解指定的生成的對象為原始類的對象,不是Mock/Spy對象。以上可參考示例TestInjectMocks1、TestInjectMocks2類。

當使用@InjectMocks注解指定某類的執行個體時,對于未使用@Mock/@Spy注解指定的成員變量,不會被自動注入,即值為null。可參考示例TestInjectMocks3類。

1.2.1. 不推薦使用@InjectMocks注解

不推薦使用@InjectMocks注解實作Mock/Spy對象的自動注入,當需要使用與@InjectMocks注解類似的功能時,可以使用Whitebox.setInternalState()等方法通過反射的方式對成員變量進行替換。不推薦的原因如下:

  • 使用不友善

對于@InjectMocks注解指定的類的執行個體,需要使用@Mock/@Spy注解指定每個成員變量,未指定的成員變量值會為null;

  • 執行成員變量的真實方法不友善

對于@InjectMocks注解指定的類的執行個體,注入的成員變量為Mock/Spy對象,當需要執行成員變量的真實方法時不友善。

1.3. MockitoAnnotations.initMocks()方法

在測試類級别使用@PrepareForTest注解時,若未執行MockitoAnnotations.initMocks()方法,測試代碼執行時使用了@InjectMocks、@Mock注解的對象為null,使用@Spy注解的對象不是null;在測試代碼中執行MockitoAnnotations.initMocks()方法後,@InjectMocks、@Mock注解對應的對象會變為非null。若未在測試類中使用@InjectMocks、@Mock注解,則不需要調用MockitoAnnotations.initMocks()方法。

示例如下,可參考示例TestInitMocksNoPrepare、TestInitMocksWithPrepare1、TestInitMocksWithPrepare2類。

// 在測試類級别指定
@PrepareForTest({})

@InjectMocks
private TestServiceC1Impl testServiceC1Impl;

@Mock
private TestServiceA1 testServiceA1Mock;

@Before
public void init() {
	assertNull(testServiceC1Impl);

	assertNull(testServiceA1Mock);

	MockitoAnnotations.initMocks(this);

	assertNotNull(testServiceC1Impl);

	assertNotNull(testServiceA1Mock);
}
           

1.4. Mockito.reset()方法

Mockito.reset()方法可以對Mock進行重置。參考 https://static.javadoc.io/org.mockito/mockito-core/latest/org/mockito/Mockito.html#resetting_mocks 。良好的單元測試代碼幾乎不需要使用該功能。通常,不需要對Mock進行重置,隻需要為每個測試方法建立新的Mock即可。

1.5. Mockito的Stub操作支援的對象

Mockito的Stub操作(Mockito.when()、Mockito.do…().when()等方法),隻支援Mock/Spy對象,不支援原始對象。可參考示例TestStubSupport類。

1.6. 擷取私有成員變量

當需要擷取私有成員變量時,可使用Whitebox.getInternalState()方法,支援指定變量類型(對應類型的變量隻存在一個)或變量名稱擷取。

也可以使用PowerMockito.field().get()方法擷取私有成員變量,但不如Whitebox.getInternalState()方法簡單。

示例如下,可參考示例TestGetPrivateField類。

@Autowired
private TestPublicNonVoidService1 testPublicNonVoidService1;
	
TestTableMapper testTableMapper1 = (TestTableMapper) PowerMockito.field(TestPublicNonVoidService1Impl.class,
		"testTableMapper").get(testPublicNonVoidService1);

TestTableMapper testTableMapper2 = Whitebox.getInternalState(testPublicNonVoidService1, TestTableMapper.class);

TestTableMapper testTableMapper3 = Whitebox.getInternalState(testPublicNonVoidService1, "testTableMapper");
           

1.6.1. 檢查對象字段值是否等于預期值的簡化方法

以下方法使用了Whitebox.getInternalState()方法。

示例代碼中提供了檢查對象字段值是否等于預期值的簡化方法:TestCommonUtil.checkObjectValue(),支援對字元串、int、long、BigDecimal、Date等類型進行比較,可以避免對需要檢查的對象的每個字段值逐個進行assertEquals等判斷,通過數組指定需要比較的字段名稱及預期值,可以減少代碼量。

以上方法參數1為需要檢查的對象,參數2為Object數組,内容為需要檢查的資訊,元素應為偶數個,下标為奇數的元素為key,下标為偶數的元素為value,使用示例如下:

Date date = new Date();

TestTable2 testTable2 = new TestTable2();
testTable2.setId(TestConstants.FLAG1);
testTable2.setChar1(TestConstants.FLAG1);
testTable2.setChar2(TestConstants.FLAG1);
testTable2.setText2(TestConstants.FLAG1);
testTable2.setInt1(1);
testTable2.setInt2(2);
testTable2.setDecimal1(100L);
testTable2.setDecimal2(BigDecimal.TEN);
testTable2.setDatetime1(date);
testTable2.setDatetime2(date);
testTable2.setTimestamp1(date);
testTable2.setTimestamp2(date);

TestCommonUtil.checkObjectValue(testTable2, new Object[]{"id", TestConstants.FLAG1, "char1", TestConstants.FLAG1,
		"char2", TestConstants.FLAG1, "text2", TestConstants.FLAG1, "int1", 1, "int2", 2, "decimal1", 100L, "decimal2", BigDecimal.TEN,
		"datetime1", date, "datetime2", date, "timestamp1", date, "timestamp2", date});
           

可參考示例TestCheckObjectValue類。

1.7. 替換私有成員變量

當需要替換私有成員變量時,可使用Whitebox.setInternalState()方法,支援指定變量類型,或變量名稱對變量進行替換,或直接指定變量進行替換(不需要指定變量類型或名稱,需要滿足對應類型的變量隻存在一個);當直接指定變量進行替換時,支援一次替換多個變量。

也可以使用PowerMockito.field().set()方法替換私有成員變量,但不如Whitebox.setInternalState()方法簡單。

示例如下,可參考示例TestSetPrivateField類。

TestNonStatic2 testNonStatic2 = new TestNonStatic2();

PowerMockito.field(TestNonStatic2.class, "flag").set(testNonStatic2, TestConstants.FLAG1);

Whitebox.setInternalState(testNonStatic2, String.class, TestConstants.FLAG2);

Whitebox.setInternalState(testNonStatic2, TestConstants.FLAG3);

Whitebox.setInternalState(testNonStatic1, "str1", TestConstants.FLAG1);
           

1.8. 建立構造函數為私有的類的執行個體

對于構造函數為私有的類,當需要建立執行個體時,可以調用Whitebox.newInstance()或Whitebox.invokeConstructor()方法,示例如下:

TestPrivateConstructor2 testPrivateConstructor2 = Whitebox.newInstance(TestPrivateConstructor2.class);
TestPrivateConstructor2 testPrivateConstructor2 = Whitebox.invokeConstructor(TestPrivateConstructor2.class);
           

以上方法的差別在于,Whitebox.newInstance()方法不會調用構造函數,Whitebox.invokeConstructor()方法會調用構造函數。可參考示例TestNewPrivateConstructor1、TestNewPrivateConstructor2類。

1.9. 執行私有方法

當需要執行私有方法時,可使用Whitebox.invokeMethod()方法,支援執行靜态私有方法及非靜态私有方法。

也可以使用PowerMockito.method().invoke()方法執行私有方法,但不如Whitebox.invokeMethod()方法簡單。

示例如下,可參考示例TestInvokePrivateMethod類。

@Autowired
private TestPrivateNonVoidService1 testPrivateNonVoidService1;
	
String str = Whitebox.invokeMethod(TestStaticPrivateNonVoid1.class, TestStaticPrivateNonVoid1.NAME_TEST3);

String str = Whitebox.invokeMethod(testPrivateNonVoidService1, TestPrivateNonVoidService1Impl.NAME_TEST1, "");

String str = (String) PowerMockito.method(TestPrivateNonVoidService1Impl.class, TestPrivateNonVoidService1Impl
		.NAME_TEST1).invoke(testPrivateNonVoidService1, "");
           

2. 使用Mock禁止Spring定時任務

在執行單元測試用例時,若滿足了定時任務的執行時間要求,會使定時任務執行,可能對測試造成幹擾,可以使用Mock禁止定時任務。

以下以使用Mock禁止Spring定時任務為例,若使用其他的定時任務架構,也可以參考。

Spring定時任務的初始化在ScheduledTaskRegistrar.scheduleTasks()方法中完成,在該方法中會分别完成triggerTasks、cronTasks、fixedRateTasks、fixedDelayTasks等定時任務的初始化。

使用Mock禁止Spring定時任務,需要使用@PrepareForTest注解指定ScheduledTaskRegistrar類,在@BeforeClass注解對應的方法中禁止ScheduledTaskRegistrar.scheduleTasks()方法,如下所示:

@PrepareForTest({ScheduledTaskRegistrar.class})

@BeforeClass
public static void beforeClass() {
    PowerMockito.suppress(PowerMockito.method(ScheduledTaskRegistrar.class, "scheduleTasks"));
}
           

可參考示例TestSpringTaskEnabled類,若不禁止Spring定時任務,在單元測試執行期間會執行Spring定時任務。

可參考示例TestSpringTaskDisabled類,使用以上方法禁止Spring定時任務後,在單元測試執行期間不會執行Spring定時任務。