powermockito测试私有方法_03 增强测试: 静态、私有方法处理
mockito 已經很強大,能幫我們完成大部分 mock 工作,但是對于一些特殊方法來說,還是無能為力。
例如,當我們使用系統獲取當前時間戳的時候,可能會調用 System.currentTimeMillis()。對于這個方法來說,我們無法 mock。往往就會遇到一個有趣的現象,一些測試過了一段時間就通不過了,項目中可能有對時間進行檢查的邏輯。
我們有一個項目是做財務報銷單,當費用產生后的幾個月后報銷就會失敗,我們的模擬數據是一個固定的時間,因此幾個月后重新運行這個項目的單元測試就不通過了。
另外,項目中不可避免的需要 mock 系統靜態方法、私有方法;以及對一些私有方法進行測試,雖然不推薦測試私有方法,但是如果遇到遺留系統,public 方法有時候巨大,測試的成本非常高。
配合 mockito 使用的另外一個框架是 powermock。它采用了字節碼技術,可以增強測試,解決 mock 靜態、私有方法,以及必要時測試靜態、私有方法。
為了便于演示,我給上一部分的例子中的 User 對象增加了 createAt 字段,createAt 字段在 register 方法內被填充,然后進行持久化。
更新后的 User 對象如下:
public class User {
private String email;
private String username;
private String password;
private Instant createAt;
public User(String email, String username, String password, Instant createAt) {
this.email = email;
this.username = username;
this.password = password;
this.createAt = createAt;
}
...
}
并給 user 添加對應的值,Instant.now() 調用的系統方法。
user.setCreateAt(Instant.now());
按照我們前面的測試,這樣會給測試帶來不變,因此需要想辦法 mock 掉 Instant.now 這個方法。
assertEquals("", argument.getValue().getCreateAt());
首先,引入 powermock 的相關依賴。powermock 有兩個模塊,一個是封裝 junit、另外一個是封裝 mockito,間接的依賴了 junit 和 mockito。因此可以先把原來的測試依賴移除,添加下面的兩個依賴即可。
org.powermock
powermock-module-junit4
2.0.2
test
org.powermock
powermock-api-mockito2
2.0.2
test
然后,使用 PowerMockRunner 代替 mockito 的 runner。并使用 @PrepareForTest 對用到該靜態方法的地方進行初始化。
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
在測試過程中,我們就可以 mock Instant.class 中的靜態方法,并且會影響 UserService 中使用它的地方。
Instant moment = Instant.ofEpochSecond(1596494464);
PowerMockito.mockStatic(Instant.class);
PowerMockito.when(Instant.now()).thenReturn(moment);
Instant.now() 就會按照我們期望的值返回結果,當然也可以按照時間戳 1596494464 來斷言了,完整的測試如下:
@RunWith(PowerMockRunner.class)
// 比如使用 PrepareForTest 讓 mock 在被測試代碼中生效
@PrepareForTest({UserService.class})
public class UserServiceAnnotationTest {
@Mock
UserRepository mockedUserRepository;
@Mock
EmailService mockedEmailService;
@Spy
EncryptionService mockedEncryptionService = new EncryptionService();
@InjectMocks
UserService userService;
@Test
public void should_register() {
// mock 前生成一個 Instant 實例
Instant moment = Instant.ofEpochSecond(1596494464);
// mock 并設定期望返回值
PowerMockito.mockStatic(Instant.class);
PowerMockito.when(Instant.now()).thenReturn(moment);
// given
User user = new User("admin@test.com", "admin", "xxx", null);
// when
userService.register(user);
// then
verify(mockedEmailService).sendEmail(
eq("admin@test.com"),
eq("Register Notification"),
eq("Register Account successful! your username is admin"));
ArgumentCaptor argument = ArgumentCaptor.forClass(User.class);
verify(mockedUserRepository).saveUser(argument.capture());
assertEquals("admin@test.com", argument.getValue().getEmail());
assertEquals("admin", argument.getValue().getUsername());
assertEquals("cd2eb0837c9b4c962c22d2ff8b5441b7b45805887f051d39bf133b583baf6860", argument.getValue().getPassword());
assertEquals(moment, argument.getValue().getCreateAt());
}
}
除了入門的基本使用外,powermock 還有一些需要特別注意的點,可以避免在實際項目中碰到的問題。
@PrepareForTest 中的參數,也就是一個需要被準備的類。這個類不是被 Mock 的類本身。例如上面一個例子中,我們需要 Mock 的是 Instant.now(),這個方法在 UserService 中被使用,我們需要 prepare UserService 而不是 Instant。這是 powermock 在使用中最常見的一個錯誤,就其原因是 靜態方法是類級別的方法,需要在類被加載前準備完畢。
具體的實現是在 PowerMockRunner 中完成的,其中用了很多字節碼級別的技術,可以不用關心具體實現。
上面的例子中,我們不需要驗證 Instant.now 的調用情況。在有些情況下,對靜態方法也需要驗證。
可以使用 PowerMockito 的 verifyStatic 方法,然后直接調用被 verify 的靜態方法即可。
PowerMockito.verifyStatic(Static.class);
Static.thirdStaticMethod(Mockito.anyInt());
需要注意的是 verifyStatic 需要每次驗證都調用,這兩句代碼為成對出現的。
有時候被測試的代碼中可能會直接使用 new 關鍵字創建一個對象,這種情況下就不太好隔離被創建的對象。如果不使用 powermock 甚至這段代碼不能被測試,有兩個途徑來解決。一個是使用使用工廠方法或者依賴注入代替直接使用 new 關鍵字,進行解耦;另一種方式是,使用 powermock 對構造方法進行 mock。
在 powermock 中使用 whenNew 這個方法,可以攔截構造方法的調用,而直接返回其他對象或者異常。構造方法的 mock 是 powermock 中最常用的特性之一。
@RunWith(PowerMockRunner.class)
@PrepareForTest(X.class)
public class XTest {
@Test
public void test() {
whenNew(MyClass.class).withNoArguments().thenThrow(new IOException("error message"));
X x = new X();
x.y(); // y is the method doing "new MyClass()"
..
}
}
和前面的問題類似,在做一些重構時,我們發現類中有一些特別長的私有方法,這些私有方法比較復雜帶來的測試成本很高。
一種方式是,重構這些私有方法到另外一個類中,保持類的私有方法處于較少的狀態。另外一種是通過 powermock 對私有方法進行 mock。使用 powermock mock 私有方法非常簡單,只需要 PowerMockito.when() 方法即可,因為私有方法直接調用會有 java 語法報錯,因此when 方法提供了通過方法名 mock 的重載。
假如 doSendEmockedEmailService 對象中有一個私有方法 doSendEmail,下面的示例代碼演示了 powermock mock 私有方法的例子。
PowerMockito.when(mockedEmailService, "doSendEmail").thenReturn(true));
在使用 Spring 等一些框架時,會使用 @Value 為對象屬性注入值,但是往往這個屬性是私有的。為了測試方便,mockito 提供了響應的工具。
早期的工具使用了 whitebox 類,提供統一的反射操作方法,因為 whitebox 過于開放,在后期的版本中不推薦使用了。
一般我們在項目中用的比較多的是 FieldSetter.setField 這個 API,可以比較優雅的解決這個問題。
@Value
private String template;
FieldSetter.setField(
mockedEmailService,EmailService.class.getDeclaredField("template"),
"test template"
);
一般來說,單元測試會測試 public 方法,如果被測試代碼不合理時,通常的做法是修改被測試代碼。讓代碼具備可測試性非常重要,也能讓代碼變得更加整潔。
如果我們遇到私有方法,但是想要測試有兩個方法。
一種比較好的方法是將私有方法修改為包級別私有,然后將測試代碼放到同一個包下,但是處于 test 目錄下 (src/main/java 和 src/test/java 的關系),這樣測試代碼就能訪問到該方法。
另外的方法是,使用一些輔助工具,例如 mockito 的 Whitebox 類,可以提供對私有方法、屬性的訪問。使用 Whitebox 可以在必要時增強測試能力。
Whitebox.invokeMethod(testObj, "method1", new Long(10L));
如果使用了 SpringTest 還可以使用 ReflectionTestUtils 類來完成:
ReflectionTestUtils.invokeMethod(student, "saveOrUpdate", "From Unit test");
實際工作中,被測試代碼不一定能非常容易的被 mock 和測試。讓代碼具有很好的測試性,實際開發過程中非常重要的一件事。
當我們確實需要對私有方法做 mock 和 測試時,可以借助其他方法和工具:使用 powermock 對私有方法、屬性進行 mock 和驗證
使用包級別可訪問的策略對私有方法進行改造,使其可被測試
使用反射工具,例如 Whitebox、ReflectionTestUtils、FieldSetter 訪問私有屬性和方法
總結
以上是生活随笔為你收集整理的powermockito测试私有方法_03 增强测试: 静态、私有方法处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2021牛客练习赛90
- 下一篇: 【计算机视觉】图像拼接技术