我遇到的一件事是使用模擬框架的團隊假設他們在模擬。
他們并不知道Mocks只是Gerard Meszaros在xunitpatterns.com上歸類的“測試雙打”之一。
重要的是要意識到每種類型的測試雙精度在測試中都扮演著不同的角色。 用與您需要學習不同模式或重構的方式相同,您需要了解每種類型的測試double的原始角色。 然后可以將它們組合起來以滿足您的測試需求。
我將簡要介紹這種分類的產生方式以及每種類型的不同之處。
我將在Mockito中使用一些簡短的簡單示例進行此操作。
非常簡短的歷史
多年來,人們一直在編寫系統組件的輕量級版本以幫助進行測試。 通常將其稱為存根。 在2000年的文章“ Endo-Testing:使用模擬對象進行單元測試”中介紹了模擬對象的概念。 從那時起,Meszaros將存根,模擬和其他許多類型的測試對象歸類為“測試雙打”。
該術語已由Martin Fowler在“ Mocks Are n't Stubs”中引用,并在Microsoft社區中被采用,如“ Exploring The Test Doubles Continuum of Test Doubles”中所示。
參考部分中顯示了指向這些重要論文的鏈接。
考試雙打的類別

上圖顯示了常用的雙重測試類型。 以下URL提供了對每個模式及其功能以及替代術語的很好的交叉引用。
http://xunitpatterns.com/Test%20Double.html
莫基托
Mockito是一個測試間諜框架,學習起來非常簡單。 Mockito值得注意的是,在測試之前沒有定義任何模擬對象的期望,因為它們有時在其他模擬框架中也是如此。 開始嘲笑時,這會導致更自然的樣式(IMHO)。
以下示例在此處純粹是為了簡單演示如何使用Mockito實施不同類型的測試雙打。
網站上有大量有關如何使用Mockito的特定示例。
http://docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html
使用Mockito測試雙打
以下是一些使用Mockito的基本示例,以顯示Meszaros定義的每個測試雙打的作用。
我為每個對象都提供了指向主要定義的鏈接,因此您可以獲得更多示例和完整定義。
虛擬對象
http://xunitpatterns.com/Dummy%20Object.html
這是所有測試雙打中最簡單的一次。 這是一個沒有實現的對象,僅用于填充與測試無關的方法調用的參數。
例如,下面的代碼使用很多代碼來創建客戶,這對測試并不重要。
只要客戶數恢復為1,該測試就不會在乎添加哪個客戶。
public Customer createDummyCustomer() {County county = new County('Essex');City city = new City('Romford', county);Address address = new Address('1234 Bank Street', city);Customer customer = new Customer('john', 'dobie', address);return customer;
}@Test
public void addCustomerTest() {Customer dummy = createDummyCustomer();AddressBook addressBook = new AddressBook();addressBook.addCustomer(dummy);assertEquals(1, addressBook.getNumberOfCustomers());
}
我們實際上并不關心客戶對象的內容,但是它是必需的。 我們可以嘗試使用null值,但是如果代碼正確,則可能會引發某種異常。
@Test(expected=Exception.class)
public void addNullCustomerTest() {Customer dummy = null;AddressBook addressBook = new AddressBook();addressBook.addCustomer(dummy);
}
為了避免這種情況,我們可以使用一個簡單的Mockito假人來獲得所需的行為。
@Test
public void addCustomerWithDummyTest() {Customer dummy = mock(Customer.class);AddressBook addressBook = new AddressBook();addressBook.addCustomer(dummy);Assert.assertEquals(1, addressBook.getNumberOfCustomers());
}
正是這個簡單的代碼創建了一個要傳遞給調用的虛擬對象。
Customer dummy = mock(Customer.class);
不要被模擬語法所迷惑-這里扮演的角色是虛擬的,而不是模擬的。
區別在于測試雙重性的作用,而不是用于創建雙重性的語法。
該類可以輕松替代客戶類,并使測試非常容易閱讀。
測試存根
http://xunitpatterns.com/Test%20Stub.html
測試存根的作用是將受控值返回到要測試的對象。 這些被描述為測試的間接輸入。 希望有一個例子可以闡明這意味著什么。
采取以下代碼
public class SimplePricingService implements PricingService
{ PricingRepository repository;public SimplePricingService(PricingRepository pricingRepository) {this.repository = pricingRepository;}@Overridepublic Price priceTrade(Trade trade) {return repository.getPriceForTrade(trade);}@Overridepublic Price getTotalPriceForTrades(Collectiontrades) {Price totalPrice = new Price();for (Trade trade : trades){Price tradePrice = repository.getPriceForTrade(trade);totalPrice = totalPrice.add(tradePrice);}return totalPrice;}
SimplePricingService具有一個協作對象,即交易存儲庫。 交易存儲庫通過getPriceForTrade方法將交易價格提供給定價服務。
為了測試SimplePricingService中的業務邏輯,我們需要控制這些間接輸入
即我們從未通過測試的輸入。 如下所示。

在以下示例中,我們對PricingRepository存根以返回可用于測試SimpleTradeService的業務邏輯的已知值。
@Test
public void testGetHighestPricedTrade() throws Exception {Price price1 = new Price(10); Price price2 = new Price(15);Price price3 = new Price(25);PricingRepository pricingRepository = mock(PricingRepository.class);when(pricingRepository.getPriceForTrade(any(Trade.class))).thenReturn(price1, price2, price3);PricingService service = new SimplePricingService(pricingRepository);Price highestPrice = service.getHighestPricedTrade(getTrades());assertEquals(price3.getAmount(), highestPrice.getAmount());
}
破壞者的例子
測試存根有兩種常見的變體:響應者和破壞者。
如前面的示例,使用響應者來測試幸福的道路。
破壞者用于測試以下異常行為。
@Test(expected=TradeNotFoundException.class)
public void testInvalidTrade() throws Exception {Trade trade = new FixtureHelper().getTrade();TradeRepository tradeRepository = mock(TradeRepository.class);when(tradeRepository.getTradeById(anyLong())).thenThrow(new TradeNotFoundException());TradingService tradingService = new SimpleTradingService(tradeRepository);tradingService.getTradeById(trade.getId());
}
模擬對象
http://xunitpatterns.com/Mock%20Object.html
模擬對象用于在測試期間驗證對象行為。 通過對象行為,我的意思是我們檢查在運行測試時是否在對象上執行了正確的方法和路徑。 這與存根的支持作用完全不同,存根用于為您要測試的任何內容提供結果。 在存根中,我們使用為方法定義返回值的模式。
when(customer.getSurname()).thenReturn(surname);
在模擬中,我們使用以下形式檢查對象的行為。
verify(listMock).add(s);
這是一個簡單的示例,我們要測試新交易是否已正確審核。
這是主要代碼。
public class SimpleTradingService implements TradingService{TradeRepository tradeRepository;AuditService auditService;public SimpleTradingService(TradeRepository tradeRepository, AuditService auditService){this.tradeRepository = tradeRepository;this.auditService = auditService;}public Long createTrade(Trade trade) throws CreateTradeException {Long id = tradeRepository.createTrade(trade);auditService.logNewTrade(trade);return id;
}
以下測試為交易存儲庫創建存根,并為AuditService創建模擬
然后,我們在模擬的AuditService上調用verify,以確保TradeService調用了
logNewTrade方法正確
@Mock
TradeRepository tradeRepository;@Mock
AuditService auditService;@Test
public void testAuditLogEntryMadeForNewTrade() throws Exception { Trade trade = new Trade('Ref 1', 'Description 1');when(tradeRepository.createTrade(trade)).thenReturn(anyLong()); TradingService tradingService = new SimpleTradingService(tradeRepository, auditService);tradingService.createTrade(trade);verify(auditService).logNewTrade(trade);
}
以下行對模擬的AuditService進行檢查。
verify(auditService).logNewTrade(trade);
該測試使我們能夠證明審計服務在創建交易時的行為正確。
測試間諜
http://xunitpatterns.com/Test%20Spy.html
值得一看上面的鏈接,以嚴格定義測試間諜。
但是,在Mockito中,我喜歡使用它來包裝實際對象,然后驗證或修改其行為以支持您的測試。 這是一個示例,我們檢查了列表的標準行為。 注意,我們既可以驗證是否調用了add方法,也可以斷言該項目已添加到列表中。
@Spy
List listSpy = new ArrayList();@Test
public void testSpyReturnsRealValues() throws Exception {String s = 'dobie';listSpy.add(new String(s));verify(listSpy).add(s);assertEquals(1, listSpy.size());
}
將其與僅可驗證方法調用的模擬對象進行比較。 因為我們僅模擬列表的行為,所以它不記錄已添加項目,并且在調用size()方法時返回默認值零。
@Mock
ListlistMock = new ArrayList();@Test
public void testMockReturnsZero() throws Exception {String s = 'dobie';listMock.add(new String(s));verify(listMock).add(s);assertEquals(0, listMock.size());
}
testSpy的另一個有用功能是能夠對返回調用進行存根。 完成此操作后,該對象將正常運行,直到調用存根方法為止。
在此示例中,我們將get方法存根以始終引發RuntimeException。 其余行為保持不變。
@Test(expected=RuntimeException.class)
public void testSpyReturnsStubbedValues() throws Exception {listSpy.add(new String('dobie')); assertEquals(1, listSpy.size());when(listSpy.get(anyInt())).thenThrow(new RuntimeException());listSpy.get(0);
}
在此示例中,我們再次保留了核心行為,但更改了size()方法,使其最初返回1,對所有后續調用返回5。
public void testSpyReturnsStubbedValues2() throws Exception {int size = 5;when(listSpy.size()).thenReturn(1, size);int mockedListSize = listSpy.size();assertEquals(1, mockedListSize);mockedListSize = listSpy.size();assertEquals(5, mockedListSize); mockedListSize = listSpy.size();assertEquals(5, mockedListSize);
}
這真是不可思議!
假物件
http://xunitpatterns.com/Fake%20Object.html
假物品通常是手工制作或重量較輕的物品,僅用于測試,不適合生產。 一個很好的例子是內存數據庫或偽造的服務層。
它們往往提供比標準測試倍增功能更多的功能,因此通常不適合使用Mockito進行實現。 這并不是說它們不能像這樣構造,僅僅是因為它可能不值得采用這種方式來實現。
參考:“ 小事半解 ” – 敏捷工程技術博客上來自我們JCG合作伙伴 John Dobie的Mockito了解測試雙打 。
翻譯自: https://www.javacodegeeks.com/2012/05/mocks-and-stubs-understanding-test.html