單元測試中Mock與Stub的淺析

Github Repohtml

mocksArentStubsgit

本部分主要介紹所謂的Test Double的概念,而且對其中容易被混用的Mocks與Stubs的概念進行一個闡述。在初期接觸到的時候,不少人會把Mock對象與另外一個單元測試中常常用到的Stub對象搞混掉。爲了方便更好地理解,這裏把全部的所謂的Test Double的概念進行一個說明。咱們先來看一個經常使用的單元測試的用例:github

public class OrderEasyTester extends TestCase {
  private static String TALISKER = "Talisker";
  
  private MockControl warehouseControl;
  private Warehouse warehouseMock;
  
  public void setUp() {
    warehouseControl = MockControl.createControl(Warehouse.class);
    warehouseMock = (Warehouse) warehouseControl.getMock();    
  }

  public void testFillingRemovesInventoryIfInStock() {
    //setup - data
    Order order = new Order(TALISKER, 50);
    
    //setup - expectations
    warehouseMock.hasInventory(TALISKER, 50);
    warehouseControl.setReturnValue(true);
    warehouseMock.remove(TALISKER, 50);
    warehouseControl.replay();

    //exercise
    order.fill(warehouseMock);
    
    //verify
    warehouseControl.verify();
    assertTrue(order.isFilled());
  }

  public void testFillingDoesNotRemoveIfNotEnoughInStock() {
    Order order = new Order(TALISKER, 51);    

    warehouseMock.hasInventory(TALISKER, 51);
    warehouseControl.setReturnValue(false);
    warehouseControl.replay();

    order.fill((Warehouse) warehouseMock);

    assertFalse(order.isFilled());
    warehouseControl.verify();
  }
}

當咱們進行單元測試的時候,咱們會專一於軟件中的一個小點,不過問題就是雖然咱們只想進行一個單一模塊的測試,可是不得不依賴於其餘模塊,就好像上面例子中的warehouse。而在我提供的兩種不一樣的測試用例的編寫方案中,第一個是使用了真實的warehouse對象,而第二個使用了所謂的mock的warehouse對象,也意味着並非一個真正的warehouse。使用Mock對象也是一種經常使用的在測試中避免依賴真正的對象的方法,不過像這種在測試中不使用真正對象的方法也有不少。數據庫

咱們常常看到的相似的關聯的名詞會有:stub、mock、fake、dummy。本文中我是打算借鑑Gerard Meszaros的論述,可能並非全部人都怎麼描述,不過我以爲Gerard Meszaros說的不錯。Gerard Meszaros是用Test Double這個術語來稱呼這一類用於替換真實對象的模擬對象。Gerard Meszaros具體定義瞭如下幾類double:編程

  • Dummy : 用於傳遞給調用者可是永遠不會被真實使用的對象,一般它們只是用來填滿參數列表。單元測試

  • Fake : Fake對象經常與類的實現一塊兒起做用,可是隻是爲了讓其餘程序可以正常運行,譬如內存數據庫就是一個很好的例子。測試

  • Stubs : Stubs一般用於在測試中提供封裝好的響應,譬若有時候編程設定的並不會對全部的調用都進行響應。Stubs也會記錄下調用的記錄,譬如一個email gateway就是一個很好的例子,它能夠用來記錄全部發送的信息或者它發送的信息的數目。簡而言之,Stubs通常是對一個真實對象的封裝。code

  • Mocks : Mocks也就是Fowler這篇文章討論的重點,便是針對設定好的調用方法與須要響應的參數封裝出合適的對象。htm

在上述這幾種doubles中,只有mocks強調行爲驗證,其餘的通常都是強調狀態驗證。爲了更好地描述這種區別,咱們會對上面的例子進行一些擴展。通常在真實對象不太好交互或者代碼尚未寫好的時候,咱們會選擇使用一個測試的Double。譬如咱們須要測試一個發送郵件的程序是否是可以在發送郵件的時候設定正確的順序,而咱們確定不但願真的發郵件出去,這樣會被打死的。所以咱們會爲咱們的email系統來建立一個test double。這裏也是用例子來展現mocks與stubs區別的地方:對象

public interface MailService {
  public void send (Message msg);
}
public class MailServiceStub implements MailService {
  private List<Message> messages = new ArrayList<Message>();
  public void send (Message msg) {
    messages.add(msg);
  }
  public int numberSent() {
    return messages.size();
  }
}

而後就能夠進行狀態驗證了:

class OrderStateTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }

固然這是一個很是簡單的測試,咱們並無測試它是否發給了正確的人或者發出了正確的內容。而若是使用Mock的話寫法就很不同了:

class OrderInteractionTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }
}

在兩個例子中咱們都是用了test double來替代真正的mail服務,不一樣的在於stub是用的狀態驗證而mock使用的是行爲驗證。若是要基於stub編寫狀態驗證的方法,須要寫一些額外的代碼來進行驗證。而Mock對象用的是行爲驗證,並不須要寫太多的額外代碼。

相關文章
相關標籤/搜索