單元測試中Mock與Stub的淺析
本部分主要介紹所謂的Test Double的概念,并且對其中容易被混用的Mocks與Stubs的概念進行一個闡述。在初期接觸到的時候,很多人會把Mock對象與另一個單元測試中經常用到的Stub對象搞混掉。為了方便更好地理解,這里把所有的所謂的Test Double的概念進行一個說明。我們先來看一個常用的單元測試的用例:
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一般是對一個真實對象的封裝。
-
Mocks : Mocks也就是Fowler這篇文章討論的重點,即是針對設定好的調用方法與需要響應的參數封裝出合適的對象。
在上述這幾種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對象用的是行為驗證,并不需要寫太多的額外代碼。