日常 Mockito 單元測試從業者指南
已發表: 2022-03-11在敏捷時代,單元測試已經成為強制性的,並且有許多工具可以幫助進行自動化測試。 一個這樣的工具是 Mockito,它是一個開源框架,可讓您創建和配置模擬對像以進行測試。
在本文中,我們將介紹如何創建和配置模擬,並使用它們來驗證被測系統的預期行為。 我們還將深入了解 Mockito 的內部結構,以更好地了解其設計和注意事項。 我們將使用 JUnit 作為單元測試框架,但由於 Mockito 未綁定到 JUnit,即使您使用不同的框架,您也可以跟隨。
獲得 Mockito
如今,獲得 Mockito 很容易。 如果您使用的是 Gradle,只需將這一行添加到您的構建腳本中:
testCompile "org.mockito:mockito−core:2.7.7"
至於像我這樣仍然喜歡 Maven 的人,只需將 Mockito 添加到您的依賴項中,如下所示:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
當然,世界比 Maven 和 Gradle 要廣闊得多。 您可以自由使用任何項目管理工具從 Maven 中央存儲庫獲取 Mockito jar 工件。
接近 Mockito
單元測試旨在測試特定類或方法的行為,而不依賴於它們的依賴項的行為。 由於我們正在測試最小的“單元”代碼,因此我們不需要使用這些依賴項的實際實現。 此外,在測試不同的行為時,我們將使用這些依賴項的稍微不同的實現。 一種傳統的、眾所周知的方法是創建“存根”——適用於給定場景的接口的特定實現。 這樣的實現通常具有硬編碼的邏輯。 存根是一種測試替身。 其他種類包括假貨、模擬物、間諜、假人等。
我們將只關注兩種類型的測試替身,“模擬”和“間諜”,因為 Mockito 大量使用它們。
模擬
什麼是嘲諷? 顯然,這不是你取笑你的開發者夥伴的地方。 單元測試的模擬是當您創建一個以受控方式實現真實子系統行為的對象時。 簡而言之,模擬被用作依賴項的替代品。
使用 Mockito,您可以創建一個模擬,告訴 Mockito 在調用特定方法時要做什麼,然後在您的測試中使用模擬實例而不是真實的實例。 測試結束後,可以查詢mock,查看調用了哪些具體方法,或者以狀態改變的形式查看副作用。
默認情況下,Mockito 為 mock 的每個方法提供了一個實現。
間諜
間諜是 Mockito 創建的另一種類型的測試替身。 與模擬相反,創建間諜需要一個實例來監視。 默認情況下,間諜將所有方法調用委託給真實對象,並記錄調用了哪些方法以及使用了哪些參數。 這就是使它成為間諜的原因:它正在監視一個真實的對象。
盡可能考慮使用模擬而不是間諜。 間諜可能對測試無法重新設計為易於測試的遺留代碼很有用,但需要使用間諜來部分模擬一個類表明一個類做得太多,從而違反了單一責任原則。
構建一個簡單的例子
讓我們看一個可以編寫測試的簡單演示。 假設我們有一個UserRepository
接口,它有一個通過標識符查找用戶的方法。 我們還有密碼編碼器的概念,可以將明文密碼轉換為密碼哈希。 UserRepository
和PasswordEncoder
都是通過構造函數注入的UserService
的依賴項(也稱為協作者)。 這是我們的演示代碼的樣子:
用戶存儲庫
public interface UserRepository { User findById(String id); }
用戶
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
密碼編碼器
public interface PasswordEncoder { String encode(String password); }
用戶服務
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
此示例代碼可以在 GitHub 上找到,因此您可以下載它以與本文一起查看。
應用 Mockito
使用我們的示例代碼,讓我們看看如何應用 Mockito 並編寫一些測試。
創建模擬
使用 Mockito,創建模擬就像調用靜態方法Mockito.mock()
一樣簡單:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
注意 Mockito 的靜態導入。 對於本文的其餘部分,我們將隱含地考慮添加此導入。
導入之後,我們模擬出PasswordEncoder
,一個接口。 Mockito 不僅模擬接口,還模擬抽像類和具體的非最終類。 開箱即用,Mockito 不能模擬最終類和最終或靜態方法,但如果你真的需要它,Mockito 2 提供了實驗性的 MockMaker 插件。
另請注意, equals()
和hashCode()
方法不能被模擬。
創建間諜
要創建 spy,您需要調用 Mockito 的靜態方法spy()
並將其傳遞給一個實例以進行 spy。 調用返回對象的方法將調用真正的方法,除非這些方法被存根。 這些調用被記錄下來,並且這些調用的事實可以被驗證(參見verify()
的進一步描述)。 讓我們做一個間諜:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
創建間諜與創建模擬沒有太大區別。 此外,所有用於配置 mock 的 Mockito 方法也適用於配置 spy。
與模擬相比,間諜很少使用,但您可能會發現它們對於測試無法重構的遺留代碼很有用,其中測試它需要部分模擬。 在這些情況下,您可以創建一個 spy 並存根它的一些方法來獲得您想要的行為。
默認返回值
調用mock(PasswordEncoder.class)
返回PasswordEncoder
的一個實例。 我們甚至可以調用它的方法,但是它們會返回什麼? 默認情況下,模擬的所有方法都返回“未初始化”或“空”值,例如,數字類型(原始和裝箱)為零,布爾值為 false,大多數其他類型為空值。
考慮以下接口:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
現在考慮下面的代碼片段,它給出了從模擬方法中期望的默認值的概念:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
存根方法
新鮮的、未更改的模擬僅在極少數情況下有用。 通常,我們想要配置 mock 並定義調用 mock 的特定方法時要做什麼。 這稱為存根。
Mockito 提供了兩種存根方法。 第一種方式是“當這個方法被調用時,然後做一些事情”。 考慮以下代碼段:
when(passwordEncoder.encode("1")).thenReturn("a");
它讀起來幾乎像英語:“當passwordEncoder.encode(“1”)
被調用時,返回一個a
。”
存根的第二種方式更像是“當使用以下參數調用此模擬方法時執行某些操作”。 這種存根的方式更難閱讀,因為最後指定了原因。 考慮:
doReturn("a").when(passwordEncoder).encode("1");
帶有這種存根方法的代碼片段將顯示為:“當使用參數1
調用passwordEncoder
的encode()
方法時返回a
。”
第一種方式被認為是首選,因為它是類型安全的並且更易讀。 然而,您很少被迫使用第二種方式,例如在對 spy 的真實方法進行 stub 時,因為調用它可能會產生不希望的副作用。
讓我們簡要探討一下 Mockito 提供的 stubbing 方法。 我們將在示例中包含兩種存根方法。
返回值
thenReturn
或doReturn()
用於指定在方法調用時要返回的值。
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
要么
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
您還可以指定多個值,這些值將作為連續方法調用的結果返回。 最後一個值將用作所有進一步方法調用的結果。
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
要么
//do doReturn("a", "b").when(passwordEncoder).encode("1");
使用以下代碼段可以實現相同的目的:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
此模式也可以與其他存根方法一起使用,以定義連續調用的結果。
返回自定義響應
then()
是thenAnswer()
的別名和doAnswer()
實現相同的目的,即設置一個自定義答案,以便在調用方法時返回,如下所示:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
要么
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
thenAnswer()
採用的唯一參數是Answer
接口的實現。 它有一個帶有InvocationOnMock
類型參數的方法。
您還可以作為方法調用的結果引發異常:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…或者調用類的真實方法(不適用於接口):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
如果您認為它看起來很麻煩,那您是對的。 Mockito 提供thenCallRealMethod()
和thenThrow()
來簡化測試的這方面。
調用真實方法
顧名思義, thenCallRealMethod()
和doCallRealMethod()
調用模擬對像上的真實方法:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
調用真實方法可能對部分模擬有用,但要確保被調用的方法沒有不需要的副作用並且不依賴於對象狀態。 如果是這樣,間諜可能比模擬更適合。
如果你創建一個接口的模擬並嘗試配置一個存根來調用一個真實的方法,Mockito 將拋出一個異常信息非常豐富的消息。 考慮以下代碼段:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito 將失敗並顯示以下消息:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
感謝 Mockito 開發人員提供如此詳盡的描述!
拋出異常
thenThrow()
和doThrow()
配置一個模擬方法來拋出異常:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
要么
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito 確保拋出的異常對於該特定的存根方法有效,並且如果異常不在方法的檢查異常列表中,則會抱怨。 考慮以下:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
會導致錯誤:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
如您所見,Mockito 檢測到encode()
不能拋出IOException
。
您還可以傳遞異常的類而不是傳遞異常的實例:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
要么
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
也就是說,Mockito 無法以與驗證異常實例相同的方式驗證異常類,因此您必須遵守紀律,不要傳遞非法的類對象。 例如,以下將拋出IOException
儘管encode()
預計不會拋出檢查異常:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
使用默認方法模擬接口
值得注意的是,在為接口創建 mock 時,Mockito 會模擬該接口的所有方法。 從 Java 8 開始,接口可能包含默認方法和抽象方法。 這些方法也是模擬的,因此您需要注意使它們充當默認方法。
考慮以下示例:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
在此示例中, assertFalse()
將成功。 如果這不是您所期望的,請確保您已經讓 Mockito 調用了真正的方法,如下所示:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
參數匹配器
在前面的部分中,我們使用精確值作為參數配置了我們的模擬方法。 在這些情況下,Mockito 只是在內部調用equals()
來檢查預期值是否等於實際值。
但是,有時我們事先並不知道這些值。
也許我們只是不關心作為參數傳遞的實際值,或者我們想為更廣泛的值定義一個反應。 所有這些場景(以及更多場景)都可以通過參數匹配器來解決。 這個想法很簡單:不是提供一個精確的值,而是為 Mockito 提供一個參數匹配器來匹配方法參數。
考慮以下代碼段:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
您可以看到,無論我們將什麼值傳遞給encode()
,結果都是相同的,因為我們在第一行中使用了anyString()
參數匹配器。 如果我們用簡單的英語重寫那行,聽起來就像“當密碼編碼器被要求對任何字符串進行編碼時,然後返回字符串'exact'。”
Mockito 要求您通過匹配器或精確值提供所有參數。 因此,如果一個方法有多個參數,並且您只想對它的一些參數使用參數匹配器,那就忘了它。 你不能寫這樣的代碼:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
要修復錯誤,我們必須替換最後一行以包含a
的eq
參數匹配器,如下所示:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
在這裡,我們使用了eq()
和anyInt()
參數匹配器,但還有許多其他可用的。 有關參數匹配器的完整列表,請參閱org.mockito.ArgumentMatchers
類的文檔。
請務必注意,您不能在驗證或存根之外使用參數匹配器。 例如,您不能擁有以下內容:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito 將檢測錯位的參數匹配器並拋出InvalidUseOfMatchersException
。 應以這種方式使用參數匹配器進行驗證:
verify(mock).encode(or(eq("a"), endsWith("b")));
參數匹配器也不能用作返回值。 Mockito 不能返回anyString()
或任何東西; 存根調用時需要一個準確的值。
自定義匹配器
當您需要提供一些 Mockito 中尚不可用的匹配邏輯時,自定義匹配器會派上用場。 創建自定義匹配器的決定不應掉以輕心,因為需要以非平凡的方式匹配參數表明設計存在問題或測試變得過於復雜。
因此,在編寫自定義匹配器之前,有必要檢查是否可以通過使用一些寬鬆的參數匹配器(例如isNull()
和nullable()
來簡化測試。 如果您仍然覺得需要編寫參數匹配器,Mockito 提供了一系列方法來完成它。
考慮以下示例:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
在這裡,我們創建了hasLuck
參數匹配器並使用argThat()
將匹配器作為參數傳遞給模擬方法,如果文件名以“luck”結尾,則將其存根以返回true
。 您可以將ArgumentMatcher
視為一個函數式接口,並使用 lambda 創建它的實例(這就是我們在示例中所做的)。 不太簡潔的語法如下所示:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
如果您需要創建一個適用於原始類型的參數匹配器,那麼org.mockito.ArgumentMatchers
中還有其他幾種方法:
- charThat(ArgumentMatcher<Character> 匹配器)
- booleanThat(ArgumentMatcher<Boolean> 匹配器)
- byteThat(ArgumentMatcher<Byte> 匹配器)
- shortThat(ArgumentMatcher<Short> 匹配器)
- intThat(ArgumentMatcher<Integer> 匹配器)
- longThat(ArgumentMatcher<Long> 匹配器)
- floatThat(ArgumentMatcher<Float> 匹配器)
- doubleThat(ArgumentMatcher<Double> 匹配器)
組合匹配器
當條件太複雜而無法使用基本匹配器處理時,創建自定義參數匹配器並不總是值得的; 有時結合匹配器可以解決問題。 Mockito 提供參數匹配器來在匹配原始和非原始類型的參數匹配器上實現常見的邏輯操作('not'、'and'、'or')。 這些匹配器在org.mockito.AdditionalMatchers
類中作為靜態方法實現。
考慮以下示例:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
在這裡,我們結合了兩個參數匹配器的結果: eq("1")
和contains("a")
。 最後的表達式or(eq("1"), contains("a"))
可以解釋為“參數字符串必須等於“1”或包含“a”。
請注意,在org.mockito.AdditionalMatchers
類中列出了不太常見的匹配器,例如geq()
、 leq()
、 gt()
和lt()
,它們是適用於原始值和java.lang.Comparable
。
驗證行為
一旦使用了模擬或間諜,我們就可以verify
發生了特定的交互。 從字面上看,我們是在說“嘿,Mockito,確保使用這些參數調用此方法。”
考慮以下人工示例:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
在這裡,我們設置了一個模擬並調用它的encode()
方法。 最後一行驗證了 mock 的encode()
方法是用特定的參數值a
調用的。 請注意,驗證存根調用是多餘的; 上一個片段的目的是展示在一些交互發生後進行驗證的想法。
如果我們將最後一行更改為具有不同的參數(例如b
),則先前的測試將失敗,並且 Mockito 將抱怨實際調用具有不同的參數( b
而不是預期a
)。
參數匹配器可用於驗證,就像存根一樣:
verify(passwordEncoder).encode(anyString());
默認情況下,Mockito 會驗證該方法是否被調用一次,但您可以驗證任意數量的調用:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
verify()
的一個很少使用的特性是它能夠在超時時失敗,這主要用於測試並發代碼。 例如,如果我們的密碼編碼器在另一個線程中同時調用verify()
,我們可以編寫如下測試:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
如果在 500 毫秒或更短的時間內調用並完成encode()
,則此測試將成功。 如果您需要等待您指定的全部時間,請使用after()
而不是timeout()
:
verify(passwordEncoder, after(500)).encode("a");
其他驗證模式( times()
, atLeast()
等)可以與timeout()
和after()
結合使用以進行更複雜的測試:

// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
除了times()
,支持的驗證模式還包括only()
、 atLeast()
和atLeastOnce()
(作為atLeast(1)
的別名)。
Mockito 還允許您在一組模擬中驗證調用順序。 它不是一個經常使用的功能,但如果調用順序很重要,它可能會很有用。 考慮以下示例:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
如果我們重新排列模擬調用的順序,測試將失敗並顯示VerificationInOrderFailure
。
也可以使用verifyZeroInteractions()
來驗證是否沒有調用。 此方法接受一個或多個模擬作為參數,如果調用傳入的模擬的任何方法,則該方法將失敗。
還值得一提的是verifyNoMoreInteractions()
方法,因為它將模擬作為參數,可用於檢查對這些模擬的每個調用是否都經過驗證。
捕獲參數
除了驗證使用特定參數調用方法之外,Mockito 還允許您捕獲這些參數,以便以後可以在它們上運行自定義斷言。 換句話說,你是在說“嘿,Mockito,驗證這個方法是否被調用,並給我調用它的參數值。”
讓我們創建一個PasswordEncoder
的模擬,調用encode()
,捕獲參數並檢查它的值:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
如您所見,我們將passwordCaptor.capture()
作為encode()
的參數傳遞給驗證; 這在內部創建了一個保存參數的參數匹配器。 然後我們使用passwordCaptor.getValue()
檢索捕獲的值並使用assertEquals()
檢查它。
如果我們需要跨多個調用捕獲參數, ArgumentCaptor
允許您使用getAllValues()
檢索所有值,如下所示:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
相同的技術可用於捕獲可變數量方法參數(也稱為 varargs)。
測試我們的簡單示例
現在我們對 Mockito 有了更多的了解,是時候回到我們的演示了。 讓我們編寫isValidUser
方法測試。 下面是它的樣子:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
深入了解 API
Mockito 提供了一個可讀、方便的 API,但讓我們探索它的一些內部工作原理,以了解它的局限性並避免奇怪的錯誤。
讓我們檢查運行以下代碼段時 Mockito 內部發生了什麼:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
顯然,第一行創建了一個模擬。 Mockito 使用 ByteBuddy 創建給定類的子類。 新類對像有一個生成的名稱,如demo.mockito.PasswordEncoder$MockitoMock$1953422997
,其equals()
將用作檢查身份,而hashCode()
將返回身份哈希碼。 生成並加載類後,將使用 Objenesis 創建其實例。
讓我們看看下一行:
when(mock.encode("a")).thenReturn("1");
順序很重要:這裡執行的第一條語句是mock.encode("a")
,它將在 mock 上調用encode()
,默認返回值為null
。 所以真的,我們將null
作為when()
的參數傳遞。 Mockito 並不關心傳遞給when()
的確切值,因為它在調用該方法時將有關模擬方法調用的信息存儲在所謂的“正在進行的存根”中。 稍後,當我們調用when()
,Mockito 會拉取正在進行的存根對象並將其作為when()
的結果返回。 然後我們在返回的正在進行的存根對像上調用thenReturn(“1”)
。
第三行, mock.encode("a");
很簡單:我們正在調用存根方法。 在內部,Mockito 保存此調用以供進一步驗證並返回存根調用答案; 在我們的例子中,它是字符串1
。
在第四行( verify(mock).encode(or(eq("a"), endsWith("b")));
),我們要求 Mockito 驗證是否調用了encode()
那些具體論據。
verify()
首先執行,這將 Mockito 的內部狀態變為驗證模式。 重要的是要了解 Mockito 將其狀態保存在ThreadLocal
中。 這使得實現良好的語法成為可能,但另一方面,如果框架使用不當(例如,如果您嘗試在驗證或存根之外使用參數匹配器),則會導致奇怪的行為。
那麼 Mockito 是如何創建or
匹配器的呢? 首先,調用eq("a")
,並將一個equals
匹配器添加到匹配器堆棧中。 其次, endsWith("b")
,並將endsWith
匹配器添加到堆棧中。 最後, or(null, null)
被調用——它使用從堆棧中彈出的兩個匹配器,創建or
匹配器,並將其推入堆棧。 最後,調用encode()
。 然後,Mockito 驗證該方法是否被調用了預期的次數並使用了預期的參數。
雖然參數匹配器不能被提取到變量中(因為它改變了調用順序),但它們可以被提取到方法中。 這保留了調用順序並使堆棧保持在正確的狀態:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
更改默認答案
在前面的部分中,我們以這樣一種方式創建了我們的模擬,即當調用任何模擬方法時,它們返回一個“空”值。 此行為是可配置的。 如果 Mockito 提供的那些不合適,您甚至可以提供自己的org.mockito.stubbing.Answer
實現,但是當單元測試變得過於復雜時,這可能表明出現問題。 記住 KISS 原則!
讓我們探索 Mockito 提供的預定義默認答案:
RETURNS_DEFAULTS
是默認策略; 在設置模擬時不值得明確提及。CALLS_REAL_METHODS
使未存根的調用調用真實方法。RETURNS_SMART_NULLS
通過在使用未存根方法調用返回的對象時返回SmartNull
而不是null
來避免NullPointerException
。 您仍然會因NullPointerException
失敗,但SmartNull
為您提供更好的堆棧跟踪,其中包含調用未存根方法的行。 這使得 RETURNS_SMART_NULLS 成為RETURNS_SMART_NULLS
中的默認答案是值得的!RETURNS_MOCKS
首先嘗試返回普通的“空”值,然後在可能的情況下進行模擬,否則返回null
。 空的標準與我們之前看到的有點不同:使用RETURNS_MOCKS
創建的模擬不是為字符串和數組返回null
,而是分別返回空字符串和空數組。RETURNS_SELF
對於模擬構建器很有用。 使用此設置,如果調用的方法返回的類型等於被模擬類的類(或超類),則模擬將返回其自身的一個實例。RETURNS_DEEP_STUBS
比RETURNS_MOCKS
更深入,並創建能夠從模擬等模擬返回模擬的模擬。與RETURNS_MOCKS
,空規則是RETURNS_DEEP_STUBS
中的默認規則,因此它為字符串和數組返回null
:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
命名模擬
Mockito 允許您命名一個模擬,如果您在測試中有很多模擬並且需要區分它們,這個功能很有用。 也就是說,需要命名模擬可能是設計不佳的症狀。 考慮以下:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito 會抱怨,但是因為我們還沒有正式命名 mocks,所以我們不知道是哪一個:
Wanted but not invoked: passwordEncoder.encode(<any string>);
讓我們通過在構造時傳入一個字符串來命名它們:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
現在錯誤消息更加友好,並且清楚地指向了robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
實現多個模擬接口
有時,您可能希望創建一個實現多個接口的模擬。 Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. 考慮以下示例:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
其他設置
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.