测试双打:模拟,假人和存根
大多數班級都有合作者。 在進行單元測試時,您通常希望避免使用那些協作者的實際實現方式來避免測試的脆弱性和綁定/耦合,而應使用測試雙打:模擬,存根和雙打。 本文引用了有關該主題的兩篇現有文章:Martin Fowler的Mocks Are n't Stubs和Bob Uncle的The Little Mocker 。 我都推薦他們。
術語
我將從Gerard Meszaros的書xUnit Test Patterns中借用一個術語。 在其中,他引入了術語“ 被測系統( SUT )”,即我們正在測試的東西。 “測試中的類”是更適用于面向對象的世界的一種替代方法,但是我會堅持使用SUT,因為Fowler也會這樣做。
我還將使用狀態驗證和行為驗證這兩個術語。 狀態驗證是通過檢查SUT或其協作者的狀態來驗證代碼是否正常工作。 行為驗證是在驗證協作者是否按照我們期望的方式被調用或調用。
測試雙打
好,回到如何與被測系統的合作者打交道。 對于SUT的每個協作者,您可以使用該協作者的實際實現。 例如,如果您有一個與數據訪問對象(DAO)協作的服務,如下面的WidgetService示例中所示,則可以使用真實的DAO實現。 但是,它很可能與數據庫沖突,這絕對不是我們要進行單元測試所需的數據庫。 另外,如果DAO實現中的代碼發生更改,則可能導致我們的測試開始失敗。 我個人不喜歡當被測代碼本身未更改時測試開始失敗。
因此,我們可以使用有時稱為“測試雙打”的測試。 “測試雙打”一詞也來自Meszaros的xUnit測試模式書。 他將它們描述為“為了明確運行測試而安裝的代替實際組件的任何對象或組件”。
在本文中,我將介紹我使用的三種主要的測試雙打類型:模擬,存根和傻瓜。 我還將簡要介紹兩個我很少明確使用的東西:間諜和假貨。
1.嘲弄
首先,“模擬”是一個過載的術語。 它通常用作任何測試雙精度測試的總稱; 也就是說,任何類型的對象都可以代替測試中的類中的真實協作者。 我對此感到滿意,因為大多數模擬框架都支持此處討論的大多數測試雙打。 但是,出于本文的目的,我將以更嚴格,更有限的含義使用模擬。
具體來說, 模擬是一種使用行為驗證的測試替身類型 。
馬丁·福勒(Martin Fowler)將模擬描述為“用期望進行預編程的對象,這些對象構成了期望接收的調用的規范”。 正如Bob叔叔所說的那樣,模擬程序會監視正在測試的模塊的行為,并且知道期望的行為。 一個例子可以使它更清楚。
想象一下WidgetService的實現:
public class WidgetService {final WidgetDao dao;public WidgetService(WidgetDao dao) {this.dao = dao;}public void createWidget(Widget widget) {//misc business logic, for example, validating widget is valid//...dao.saveWidget(widget);} }我們的測試可能看起來像這樣:
public class WidgetServiceTest {//test fixturesWidgetDao widgetDao = mock(WidgetDao.class);WidgetService widgetService = new WidgetService(widgetDao);Widget widget = new Widget();@Testpublic void createWidget_saves_widget() throws Exception {//call method under testwidgetService.createWidget(widget);//verify expectationverify(widgetDao).saveWidget(widget);} }我們創建了一個WidgetDao的模擬,并驗證它是否如預期的那樣被調用。 我們還可以告訴模擬程序在調用時如何響應。 這是模擬的重要組成部分,允許您操縱模擬,以便可以測試代碼的特定單元,但是在這種情況下,測試不是必需的。
模擬框架
在此示例中,我將Mockito用于模擬框架,但Java空間中還有其他對象,包括EasyMock和JMock 。
自己動手玩?
請注意,您不必使用模擬框架即可使用模擬。 您也可以自己編寫模擬,甚至可以在模擬中構建斷言。 例如,在這種情況下,我們可以創建一個名為WidgetDaoMock的類,該類實現WidgetDao接口,并且該類的createWidget()方法的實現僅記錄其被調用的情況。 然后,您可以驗證呼叫是否按預期進行。 盡管如此,現代的模擬框架仍然使這種“勞碌自在”的解決方案變得多余。
2.存根
存根是為了測試目的而“存根”或提供實現的大大簡化版本的對象。
例如,如果我們的WidgetService類現在也也依賴于ManagerService。 請參閱此處的標準化方法:
public class WidgetService {final WidgetDao dao;final ManagerService manager;public WidgetService(WidgetDao dao, ManagerService manager) {this.dao = dao;this.manager = manager;}public void standardize(Widget widget) {if (manager.isActive()) {widget.setStandardized(true);}}public void createWidget(Widget widget) {//omitted for brevity} }并且我們想測試當管理器處于活動狀態時,標準化方法是否“標準化”了一個小部件,我們可以使用如下所示的存根:
public class WidgetServiceTest {WidgetDao widgetDao = mock(WidgetDao.class);Widget widget = new Widget();class ManagerServiceStub extends ManagerService {@Overridepublic boolean isActive() {return true;}}@Testpublic void standardize_standardizes_widget_when_active() {//setupManagerServiceStub managerServiceStub = new ManagerServiceStub();WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);//call method under testwidgetService.standardize(widget);//verify stateassertTrue(widget.isStandardized());} }由于模擬通常用于行為驗證,而存根可用于狀態驗證或行為驗證。
該示例非常基礎,也可以使用模擬來完成,但是存根可以為測試夾具的可配置性提供一種有用的方法。 我們可以對ManagerServiceStub進行參數化,以使其將“活動”字段的值用作構造函數參數,因此可以在否定測試用例中重用。 也可以使用更復雜的參數和行為。 其他選項包括將存根創建為匿名內部類,或為存根創建基類,例如ManagerServiceStubBase,以供其他人擴展。 后者的優點是,如果ManagerService接口發生更改,則只有ManagerServiceStubBase類會中斷,并且需要更新。
我傾向于經常使用存根。 我喜歡他們提供的靈活性,以便能夠自定義測試裝置,并提供純Java代碼提供的清晰度。 將來的維護者不需要能夠理解某個框架。 我的大多數同事似乎更喜歡使用模擬框架。 找到最適合您的方法,并運用最佳判斷。
3.假人
顧名思義,虛擬對象是非常愚蠢的類。 它幾乎不包含任何內容,基本上只足以使您的代碼得以編譯。 當您不在乎如何使用虛擬對象時,可以將其傳遞給某些對象。 例如,作為測試的一部分,當您必須傳遞參數時,但是您不希望使用該參數。
例如,在前面的示例中的standardize_standardizes_widget_when_active()測試中,我們仍然繼續使用模擬的WidgetDao。 虛擬對象可能是一個更好的選擇,因為我們根本不希望在createWidget()方法中完全使用WidgetDao。
public class WidgetServiceTest {Widget widget = new Widget();class ManagerServiceStub extends ManagerService {@Overridepublic boolean isActive() {return true;}}class WidgetDaoDummy implements WidgetDao {@Overridepublic Widget getWidget() {throw new RuntimeException("Not expected to be called");}@Overridepublic void saveWidget(Widget widget) {throw new RuntimeException("Not expected to be called");}}@Testpublic void standardize_standardizes_widget_when_active() {//setupManagerServiceStub managerServiceStub = new ManagerServiceStub();WidgetDaoDummy widgetDao = new WidgetDaoDummy();WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);//call method under testwidgetService.standardize(widget);//verify stateassertTrue(widget.isStandardized());} }在這種情況下,我創建了一個內部類。 在大多數情況下,由于Dummy功能很少會在測試之間發生變化,因此創建非內部類并為所有測試重用更為有意義。
還要注意在這種情況下,使用模擬框架創建類的模擬實例也是可行的選擇。 我個人很少使用假人,而是創建這樣的模擬:
WidgetDaoDummy widgetDao = mock(WidgetDao.class);盡管可以肯定的是,當確實發生意外調用時,拋出異常會更加困難(這取決于您選擇的模擬框架),但是它確實具有簡潔性的巨大優勢。 虛擬變量可能很長,因為它們需要在接口中實現每種方法。
與存根一樣,假人可用于狀態或行為驗證。
間諜與偽造
我將簡要介紹另外兩種測試雙打:間諜和偽造。 我之所以簡短地說,是因為我個人很少自己明確使用這兩種類型的雙打,而且還因為術語可能會引起混亂,而又不會引起更多細微差別! 但是為了完整性……
間諜
當您想確保系統調用了某個方法時,可以使用間諜程序。 它還可以記錄各種事情,例如計算調用次數,或記錄每次傳遞的參數。
但是,對于間諜來說,存在將測試與代碼實現緊密耦合的危險。
間諜專用于行為驗證。
大多數現代的模擬框架也很好地涵蓋了這種功能。
假貨
馬丁·福勒(Martin Fowler)對偽造品的描述如下:偽造品具有有效的實現方式,但通常采取一些捷徑,這使其不適合生產(內存數據庫是一個很好的例子)。
我個人很少使用它們。
結論
測試雙打是單元測試不可或缺的一部分。 嘲笑,存根和雙打都是有用的工具,了解它們之間的差異很重要。
從最嚴格的意義上講,模擬只是使用行為驗證的雙精度形式。 指定了兩倍的期望值,然后在調用SUT時進行驗證。 但是,工作模擬也已經變得越來越籠統地描述了此處描述的任何雙打,實際上大多數現代模擬框架都可以這種通用方式使用。
最后,您應該使用哪種雙精度型? 這取決于所測試的代碼,但是我建議您遵循使您的測試意圖最清楚的任何方式進行指導。
資料來源
- 莫蒂不是存根作者 ,馬丁·福勒(Martin Fowler)
- 小嘲笑 ,“叔叔”鮑勃·馬丁
- xUnit測試模式 ,作者Gerard Meszaros
翻譯自: https://www.javacodegeeks.com/2015/11/test-doubles-mocks-dummies-and-stubs.html
總結
以上是生活随笔為你收集整理的测试双打:模拟,假人和存根的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: teamcity_TeamCity工件:
- 下一篇: 2015年Devoxx比利时–最后的想法