Mockito 中的 Stub Getter 和 Setter
1. 簡介
雖然 Mockito 提供了一種避免初始化我們不想初始化的物件的絕佳方法,但有時它的開箱即用功能是有限的。
在本教程中,我們將探討在單元測試上下文中對 setter 和 getter 進行存根的各種方法。
2. 模擬對象與實際對象
在編寫任何測試之前,讓我們先了解存根和模擬之間的差異。我們使用術語存根 (stub) 來描述在行為方面不提供任何靈活性的物件。相反,模擬允許可配置行為並提供驗證功能。
為了幫助我們查看與真實場景有些相似的範例, ExampleService
類別呼叫其他物件的方法(無論是否存根):
public class ExampleService {
public <T> T getField(Supplier<T> getter) {
return getter.get();
}
public <T> void setField(Consumer<T> setter, T value) {
setter.accept(value);
}
}
ExampleService
實作時考慮了可重用性,因此對getField()
和setField()
方法都進行了參數選擇。簡而言之, getField()
方法呼叫它接受的任何Supplier
,因此在我們的例子中是 getter 方法。相反, setField()
方法使用提供的值來呼叫它接受的任何Consumer
- 在我們的例子中是 setter 方法。
為了清楚起見,讓我們使用ExampleService
來寫一些 getter 和 setter 呼叫的範例:
exampleService.getField(() -> fooBar.getFoo()); // invokes getFoo getter
exampleService.getField(fooBar::getBar); // invokes getFoo getter
exampleService.setField((bar) -> fooBar.setBar(bar), "newBar"); // invokes bar setter
exampleService.setField(fooBar::setBar, "newBar"); // invokes bar setter
開始編寫測試之前的最後一步是定義我們將使用的模型,即SimpleClass
:
public class SimpleClass {
private Long id;
private String name;
// getters, setters, constructors
首先,模擬一個相對輕量且易於初始化的物件通常不是最佳的。事實上,我們需要編寫來模擬一個物件的程式碼比僅僅創建該物件的實例要長得多。 以下程式碼片段包含一個帶有存根的 setter 和 getter 的模擬物件以及末尾的驗證:
@Test
public void givenMockedSimpleClass_whenInvokingSettersGetters_thenInvokeMockedSettersGetters() {
Long mockId = 12L;
String mockName = "I'm 12";
SimpleClass simpleMock = mock(SimpleClass.class);
when(simpleMock.getId()).thenReturn(mockId);
when(simpleMock.getName()).thenReturn(mockName);
doNothing().when(simpleMock).setId(anyLong());
doNothing().when(simpleMock).setName(anyString());
ExampleService srv = new ExampleService();
srv.setField(simpleMock::setId, 11L);
srv.setField(simpleMock::setName, "I'm 11");
assertEquals(srv.getField(simpleMock::getId), mockId);
assertEquals(srv.getField(simpleMock::getName), mockName);
verify(simpleMock).getId();
verify(simpleMock).getName();
verify(simpleMock).setId(eq(11L));
verify(simpleMock).setName(eq("I'm 11"));
}
為了更清楚地了解情況,這裡有一個相同的測試,但沒有模擬:
@Test
public void givenActualSimpleClass_whenInvokingSettersGetters_thenInvokeActualSettersGetters() {
Long id = 1L;
String name = "I'm 1";
SimpleClass simple = new SimpleClass(id, name);
ExampleService srv = new ExampleService();
srv.setField(simple::setId, 2L);
srv.setField(simple::setName, "I'm 2");
assertEquals(srv.getField(simple::getId), simple.getId());
assertEquals(srv.getField(simple::getName), simple.getName());
}
當比較這兩個測試案例時,很明顯地模擬變體長了八行。一般來說,模擬存根的設定和驗證會導致更長的測試案例。
3. 簡單的模擬
使用 Mockito 存根最常見的情況是當建立物件需要多行程式碼或其初始化速度很慢導致測試套件效能不佳時。方便的是,Mockito 的when()
和thenReturn()
方法提供了一種避免創建真實物件的方法。對於這個例子,我們需要一個比SimpleClass,
所以讓我們引入NonSimpleClass
:
public class NonSimpleClass {
private Long id;
private String name;
private String superComplicatedField;
// getters, setters, constructors
顧名思義, superComplicatedField
需要特別處理。因此,我們必須在測試期間不要初始化它:
@Test
public void givenNonSimpleClass_whenInvokingGetName_thenReturnMockedName() {
NonSimpleClass nonSimple = mock(NonSimpleClass.class);
when(nonSimple.getName()).thenReturn("Meredith");
ExampleService srv = new ExampleService();
assertEquals(srv.getField(nonSimple::getName), "Meredith");
verify(nonSimple).getName();
}
在這種情況下,建立NonSimpleClass
的實例會導致效能問題或不必要的程式碼。相反,Mockito 存根產生了所需的測試覆蓋率,而不會引入與NonSimpleClass
實例化相關的任何負面影響。
4. 狀態模擬
Mockito 不提供開箱即用的物件狀態管理。在我們的例子中,除非我們做一些神奇的事情,否則 setter 設定的值不會由 getter 的後續呼叫傳回。對於這個問題,一個可行的解決方案是有狀態的模擬,這樣當在 setter 之後呼叫 getter 時,getter 就會傳回最後設定的值。 Wrapper
類,顧名思義,就是包裝一個值,負責狀態管理:
class Wrapper<T> {
private T value;
// getter, setter, constructors
為了利用Wrapper
類,我們使用doAnswer()
和thenAnswer()
,而不是thenReturn()
和doNothing()
Mockito 方法。這些變體允許我們存取模擬方法的參數並應用自訂邏輯,而不是簡單地傳回靜態值:
@Test
public void givenNonSimpleClass_whenInvokingGetName_thenReturnTheLatestNameSet() {
Wrapper<String> nameWrapper = new Wrapper<>(String.class);
NonSimpleClass nonSimple = mock(NonSimpleClass.class);
when(nonSimple.getName()).thenAnswer((Answer<String>) invocationOnMock -> nameWrapper.get());
doAnswer(invocation -> {
nameWrapper.set(invocation.getArgument(0));
return null;
}).when(nonSimple)
.setName(anyString());
ExampleService srv = new ExampleService();
srv.setField(nonSimple::setName, "John");
assertEquals(srv.getField(nonSimple::getName), "John");
srv.setField(nonSimple::setName, "Nick");
assertEquals(srv.getField(nonSimple::getName), "Nick");
}
我們的有狀態模擬使用底層包裝器實例來保留呼叫 setter 時設定的最新值,並在呼叫模擬的 getter 時傳回最後設定的值。
5. 結論
在這篇簡短的文章中,我們討論了各種模擬場景,並討論了何時使用模擬方便,何時不方便。此外,我們還展示了一個有狀態模擬的解決方案,這也證明了 Mockito 庫的多功能性,使我們能夠在需要時模擬相當複雜的場景。
與往常一樣,本教學的源代碼可在 GitHub 上取得。