Test Double
第一次了解Test Double
是在Martin Fowler的文章Test Double中,Gerard Meszaros提出了這個概念。雖然是06年的文章了,但里面的概念并不過時。這篇文章提到Test Double
只是一個通用的詞,代表為了達到測試目的并且減少被測試對象的依賴,使用“替身”代替一個真實的依賴對象,從而保證了測試的速度和穩定性。
在項目中,我們時常會遇到由于待測系統依賴組件無法工作而造成的測試阻礙,這是嚴重影響項目交付的風險之一,而Test Double
就是規避這個風險的手段。在測試過程中,我們使用Test Double
替代真實的依賴組件去和待測系統進行交互,Test Double
不必和真實的依賴組件的實現一模一樣,比如不用去實現依賴組件復雜的內部邏輯等,我們只需要在滿足測試需求范圍內,確保對于待測系統來說Test Double
提供的API是和依賴組件提供的一樣的,API是怎么實現的在這個上下文就顯得不重要了。基于這個特點,Test Double
多用于自動化測試比如單元測試和集成測試。
那么Test Double
就是萬能的嗎?顯然不是,畢竟我們是使用的是替代品而不是真實產品環境的配置,所以我們需要至少有一個測試去驗證使用真實依賴對象的產品。此外,要時刻注意我們是使用Test Double
去代替待測系統的依賴對象而不是直接去代替待測系統的部分功能,不然我們就在測試一個“錯誤”的產品。
Test Double
可以進一步細化為:
- Test Stub
- Test Spy
- Mock Object
- Fake Object
- Dummy Object
它們各自的定義和區別是什么呢?這篇文章會解答這個問題。
Test Stub - you can define answers to me; I'll respond the same
Test Stub
是指一個完全代替待測系統依賴組件的對象,這個對象按照我們設計的輸出與待測系統進行交互,可以理解是在待測系統內部打的一個樁。這個樁既不會與測試用例(代碼)交互,也不會在待測系統內部進行驗證。Test Stub
常用于響應待測系統的請求,然后返回特定的值。接下來,這個值會對待測系統產生影響,然后我們就在測試用例里面去驗證這個影響。
Test Stub
的實現方式一般有兩種:
-
Hard-Coded Test Stub
- 會返回固定response的Test Stub -
Configurable Test Stub
- 會根據測試需求返回相應response的Test Stub,可配置化
當我們遇到下面場景時,Test Stub
就可以派上用場
- 依賴組件無法使用,影響測試結果
- 依賴組件運行太慢,影響測試速度
- 成為
Responder
響應者,當需要給待測系統注入特定數據,從而對待測系統產生影響 - 成為
Saboteur
破壞者,當需要給待測系統注入無效數據,從而對待測系統產生異常影響,觀察待測系統如何處理錯誤情況
下面是python-doublex的Stub例子
from doublex import Stub, ANY_ARG, assert_that, is_
class Collaborator:
def hello(self):
return "hello"
def add(self, a, b):
return a + b
with Stub(Collaborator) as stub:
stub.hello().raises(SomeException)
stub.add(ANY_ARG).returns(4)
assert_that(stub.add(2,3), is_(4))
Mock Object - you can set your expectation on me
Mock Object
是指一個完全代替待測系統依賴組件,并且用于驗證待測系統輸出的對象。這個對象接受待測系統的輸出,進行處理并且這個輸出進行驗證,一旦驗證通過也會返回值給待測系統。Mock Object
主要用于接收待測系統的輸出,然后進行驗證。
Mock Object
一個重要的特點是它可以對無法在待測系統上直接被觀察到的行為或輸出進行驗證。無法觀察到的系統行為或輸出可以是數據插入數據庫,可以是數據寫入文件,也可以是對其他組件的調用。以數據庫類型Mock Object
舉例,這個Mock
的數據庫會去接受待測系統發過來的數據,并且對這個數據進行驗證,一旦驗證通過就會對數據進行處理(插入或更新操作),然后測試代碼會去驗證插入是否成功。
下面是python-doublex的Mock例子
from doublex import Mock, assert_that, verify
with Mock() as smtp:
smtp.helo()
smtp.mail(ANY_ARG)
smtp.rcpt("bill@apple.com")
smtp.data(ANY_ARG).returns(True).times(2)
smtp.helo()
smtp.mail("poormen@home.net")
smtp.rcpt("bill@apple.com")
smtp.data("somebody there?")
smtp.data("I am afraid..")
assert_that(smtp, verify())
Fake Object - you can have me with limited capabilities
Fake Object
是指一個輕量級的完全代替待測系統依賴組件的對象,采用更加簡單的方法實現依賴組件的功能。Fake Object
可以是一個“fake DB”比如簡單的內存數據庫來代替真實的重量級的數據庫,也可以是一個“fake web service”比如創建一個簡單的web service來返回指定的response。
Fake Object
和Test Stub
很類似,都是依賴組件的代替,區別就在于這個“輕量級”的定義。“輕量級”是指Fake Object
僅僅提供和依賴組件一樣的功能接口保證待測系統正常工作,讓待測系統認為Fake Object
就是“真的”依賴組件,實現細節可以非常簡單,不需要具有真實依賴組件的很多特性,也不需要像Test Stub
那樣接受測試的需求,返回特定response給待測系統。
總之,Fake Object
的實現比Test Stub
和Mock Object
簡單,所以更加可以快速滿足測試需求。不過如果我們需要控制依賴組件對待測系統的輸入或輸出,我們應該使用Test Stub
和Mock Object
。
Test Spy - monitor real ones; you can change my behavior
Test Spy
是指一個待測系統依賴組件的替身,并且會捕捉和保存待測對象對依賴系統的輸出,這個輸出會用于測試代碼中的驗證。Test Spy
主要用于記錄和驗證待測對象對依賴系統的輸出。
那和Mock Object
不同之處是什么呢?Test Spy
是把待測對象對依賴系統的輸出拿到了測試代碼里面進行驗證,這樣的話,如果待測系統的輸出不符合期望,Test Spy
并不像Mock Object
那樣第一時間讓測試失敗,而是可以在測試代碼中加入更多判斷信息,讓驗證和測試結果更加可控和可視化
下面是python-doublex的Spy例子
from hamcrest import contains_string
from doublex import Spy, assert_that, called
class Sender:
def say(self):
return "hi"
def send_mail(self, address, force=True):
pass # [some amazing code]
sender = Spy(Sender)
sender.send_mail("john.doe@example.net") # right, Sender.send_mail interface support this
assert_that(sender.send_mail, called())
assert_that(sender.send_mail, called().with_args("john.doe@example.net"))
assert_that(sender.send_mail, called().with_args(contains_string("@example.net")))
sender.bar() # interface mismatch exception
jasmine也支持Test Spy
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, 'setBar');
foo.setBar(123);
foo.setBar(456, 'another param');
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();
});
it("tracks that the spy was called x times", function() {
expect(foo.setBar).toHaveBeenCalledTimes(2);
});
it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);
expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
});
it("stops all execution on a function", function() {
expect(bar).toBeNull();
});
});
Dummy Object - pass around but never actually used
Dummy Object
對象是指為了調用被測試方法而傳入的假參數,為什么說是假參數呢?實際上這些傳入的Dummy
對象并不會對測試有任何作用,僅僅是為了成功調用被測試方法。所以,Dummy Object
又被稱為Dummy parameter或placeholder。
比如有一個類的實例創建要求傳入多個參數,這些參數里面沒有可選參數,其中有幾個參數不會對測試產生任何作用,這時我們就可以創建Dummy
對象作為假參數去創建這個實例。
模塊推薦
下面列出了一些可用的Test Double工具
Java
Python
- doublex - Powerful test doubles framework for Python
- mock - (Python standard library) A mocking and patching library
- httpretty - HTTP request mock tool for Python.