測試中 Fakes、Mocks 以及 Stubs 概念明晰
自動化測試中,我們常會使用一些經過簡化的,行為與表現類似于生產環境下的對象的復制品。引入這樣的復制品能夠降低構建測試用例的復雜度,允許我們獨立而解耦地測試某個模塊,不再擔心受到系統中其他部分的影響;這類型對象也就是所謂的 Test Double。實際上對于 Test Double 的定義與闡述也是見仁見智,Gerard Meszaros 在這篇文章中就介紹了五個不同的 Double 類型;而人們更傾向于使用 Mock 來統一描述不同的 Test Doubles。不過對于 Test Doubles 實現的誤解還是可能會影響到測試的設計,使測試用例變得混亂和脆弱,最終帶來不必要的重構。本文則是從作者個人的角度描述了常見的 Test Doubles 類型及其具體的實現:Fake、Stub 與 Mock,并且給出了不同的 Double 的使用場景。
Fake
- Fakes are objects that have working implementations, but not same as production one. Usually they take some shortcut and have simplified version of production code.Fake 是那些包含了生產環境下具體實現的簡化版本的對象。
如下圖所示,Fake 可以是某個 Data Access Object 或者 Repository 的基于內存的實現;該實現并不會真的去進行數據庫操作,而是使用簡單的 HashMap 來存放數據。這就允許了我們能夠在并沒有真的啟動數據庫或者執行耗時的外部請求的情況下進行服務的測試。
- @Profile("transient")
- public class FakeAccountRepository implements AccountRepository {
- Map<User, Account> accounts = new HashMap<>();
- public FakeAccountRepository() {
- this.accounts.put(new User("john@bmail.com"), new UserAccount());
- this.accounts.put(new User("boby@bmail.com"), new AdminAccount());
- }
- String getPasswordHash(User user) {
- return accounts.get(user).getPasswordHash();
- }
- }
除了應用到測試,Fake 還能夠用于進行原型設計或者峰值模擬中;我們能夠迅速地實現系統原型,并且基于內存存儲來運行整個系統,推遲有關數據庫設計所用到的一些決定。另一個常見的使用場景就是利用 Fake 來保證在測試環境下支付永遠返回成功結果。
Stub
- Stub is an object that holds predefined data and uses it to answer calls during tests. It is used when we cannot or don’t want to involve objects that would answer with real data or have undesirable side effects.Stub 代指那些包含了預定義好的數據并且在測試時返回給調用者的對象。Stub 常被用于我們不希望返回真實數據或者造成其他副作用的場景。
Stub 的典型應用場景即是當某個對象需要從數據庫抓取數據時,我們并不需要真實地與數據庫進行交互或者像 Fake 那樣從內存中抓取數據,而是直接返回預定義好的數據。
- public class GradesService {
- private final Gradebook gradebook;
- public GradesService(Gradebook gradebook) {
- this.gradebook = gradebook;
- }
- Double averageGrades(Student student) {
- return average(gradebook.gradesFor(student));
- }
- }
我們在編寫測試用例時并沒有從 Gradebook 存儲中抓取數據,而是在 Stub 中直接定義好需要返回的成績列表;我們只需要足夠的數據來保證對平均值計算函數進行測試就好了。
- public class GradesServiceTest {
- private Student student;
- private Gradebook gradebook;
- @Before
- public void setUp() throws Exception {
- gradebook = mock(Gradebook.class);
- student = new Student();
- }
- @Test
- public void calculates_grades_average_for_student() {
- when(gradebook.gradesFor(student)).thenReturn(grades(8, 6, 10)); //stubbing gradebook
- double averageGrades = new GradesService(gradebook).averageGrades(student);
- assertThat(averageGrades).isEqualTo(8.0);
- }
- }
Command Query Separation
僅返回部分結果而并沒有真實改變系統狀態的的方法被稱作查詢(Query)。譬如 avarangeGrades,用于返回學生成績平均值的函數就是非常典型的例子:Double getAverageGrades(Student student);。該函數僅返回了某個值,而沒有其他的任何副作用。正如我們上文中介紹的,我們可以使用 Stubs 來替換提供實際成績值的函數,從而簡化了整個測試用例的編寫。不過除了 Query 之外還有另一個類別的方法,被稱作 Command。即當某個函數在執行某些操作的時候還改變了系統狀態,不過該類型函數往往沒有什么返回值:void sendReminderEmail(Student student);。這種對于方法的劃分方式也就是 Bertrand Meyer 在Object Oriented Software Construction 一書中介紹的 Command Query 分割法。
對于 Query 類型的方法我們會優先考慮使用 Stub 來代替方法的返回值,而對于 Command 類型的方法的測試則需要依賴于 Mock。
Mock
- Mocks are objects that register calls they receive. In test assertion we can verify on Mocks that all expected actions were performed.Mocks 代指那些僅記錄它們的調用信息的對象,在測試斷言中我們需要驗證 Mocks 被進行了符合期望的調用。
當我們并不希望真的調用生產環境下的代碼或者在測試中難于驗證真實代碼執行效果的時候,我們會用 Mock 來替代那些真實的對象。典型的例子即是對郵件發送服務的測試,我們并不希望每次進行測試的時候都發送一封郵件,畢竟我們很難去驗證郵件是否真的被發出了或者被接收了。我們更多地關注于郵件服務是否按照我們的預期在合適的業務流中被調用,其概念如下圖所示:
- public class SecurityCentral {
- private final Window window;
- private final Door door;
- public SecurityCentral(Window window, Door door) {
- this.window = window;
- this.door = door;
- }
- void securityOn() {
- window.close();
- door.close();
- }
- }
在上述代碼中,我們并不想真的去關門來測試 securityOn 方法,因此我們可以設置合適的 Mock 對象:
- public class SecurityCentralTest {
- Window windowMock = mock(Window.class);
- Door doorMock = mock(Door.class);
- @Test
- public void enabling_security_locks_windows_and_doors() {
- SecurityCentral securityCentral = new SecurityCentral(windowMock, doorMock);
- securityCentral.securityOn();
- verify(doorMock).close();
- verify(windowMock).close();
- }
- }
在 securityOn 方法執行之后,window 與 door 的 Mock 對象已經記錄了所有的交互信息,這就允許我們能夠去驗證 Window 與 Door 是否被真實的調用?;蛟S有人會疑問是否在真實環境下門與窗是否被真的關閉了?其實我們并不能保證,不過這也不是我們關注的點,也不是 SecurityCentral 這個類關注的目標。門與窗是否能被正常的關閉應該是由 Door 與 Window 這兩個類所關注的。
【本文是51CTO專欄作者“張梓雄 ”的原創文章,如需轉載請通過51CTO與作者聯系】