什么是doublex
官網(wǎng)上對(duì)于doublex的介紹很簡單,就是一個(gè)Test Double的Python實(shí)現(xiàn)框架
Powerful test doubles framework for Python
也可以理解為doublex是Python的一個(gè)庫,用于實(shí)現(xiàn)Test Stub/Spy/Mock。
關(guān)于Test Double(Stub/Spy/Mock)的理解,可以參考此篇文章。
doublex的安裝
安裝有多種方式,pip依然是首選。
pip3 install doublex
doublex基本使用
doublex提供三種類型的Test Double,分別是Stub & Mock & Spy。在介紹它們的具體使用方法之前,先簡單介紹下今天例子使用的SUT:
- SUT(system under test) - playerService
- DOC(depended-on component) - dataService, profileService, bodyService, salaryService
playerService接受球員名稱和年份,然后返回一個(gè)球員的基本信息;這個(gè)基本信息由dataService球員數(shù)據(jù)和profileService球員概況組成,所以playerService依賴于dataService和profileService。playerService也返回由bodyService提供的身體指標(biāo),以及由salaryService支持的薪水服務(wù)。
由于只是為了體現(xiàn)doublex使用方法的demo,實(shí)現(xiàn)非常簡單,都是hard code。具體請(qǐng)看github。
Test Stub - Stubs tell you what you wanna hear
doublex提供了Stub()對(duì)象來創(chuàng)建Test Stub,根據(jù)參數(shù)的不同分為 Stub 和 Free Stub
Stub
假設(shè)如果dataService對(duì)象就是我們要替換的依賴對(duì)象,Stub(dataService)帶有dataService對(duì)象為參數(shù),那就代表這個(gè)Stub實(shí)例具有和dataService對(duì)象一樣的屬性,也就是說這個(gè)Stub實(shí)例可以“替換”dataService對(duì)象來與SUT進(jìn)行交互了。
下面代碼展示了Stub()基本使用場景和方法
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex_demo.Service import bodyService as bs
from doublex import Stub, ANY_ARG, assert_that, is_
class TestStub(TestCase):
def test_stub(self):
playername = "Kawhi Leonard"
#需使用with關(guān)鍵字來創(chuàng)建Stub
#Stub接受dataService類對(duì)象作為參數(shù),并且實(shí)現(xiàn)dataService類對(duì)象全部的方法
#根據(jù)dataService的實(shí)現(xiàn),get_assist()等方法不用接受參數(shù),這里的參數(shù)必須完全匹配
#get_match_number()可以根據(jù)參數(shù)的不同返回不同的值
#returns()方法定義返回值
#不能定義非dataService的屬性
with Stub(ds.dataService) as stub:
stub.get_assist().returns("6")
stub.get_score().returns("30")
stub.get_rebound().returns("10")
stub.get_match_number(2015).returns(playername + " plays 80 games at the year of 2015")
stub.get_match_number(2016).returns(playername + " plays 81 games at the year of 2016")
#使用來自于hamcrest的assert_that()和is_()做stub的驗(yàn)證
assert_that(stub.get_assist(), is_("6"))
assert_that(stub.get_score(), is_("30"))
assert_that(stub.get_rebound(), is_("10"))
assert_that(stub.get_match_number(2015), is_("Kawhi Leonard plays 80 games at the year of 2015"))
assert_that(stub.get_match_number(2016), is_("Kawhi Leonard plays 81 games at the year of 2016"))
#使用stub代替dataService,來對(duì)待測(cè)對(duì)象playerService進(jìn)行測(cè)試驗(yàn)證
player_service_stub_2016 = pls.playerService(playername, 2016, stub, pos.profileService(playername), bs.bodyService(), ss.salaryService())
assert_that(
player_service_stub_2016.get_player_info().split('\n')[0],
is_("Kawhi Leonard - san antonio spurs"))
assert_that(
player_service_stub_2016.get_player_info().split('\n')[-1],
is_("Kawhi Leonard plays 81 games at the year of 2016"))
player_service_stub_2015 = pls.playerService(playername, 2015, stub, pos.profileService(playername), bs.bodyService(), ss.salaryService())
assert_that(
player_service_stub_2015.get_player_info().split('\n')[-1],
is_("Kawhi Leonard plays 80 games at the year of 2015"))
if __name__ == '__main__':
main()
Free Stub
當(dāng)Stub()不帶參數(shù)的時(shí)候,稱之為Free Stub。由于Free Stub沒有指定被替換的依賴服務(wù),所以Free Stub的屬性不受任何限制,可以自由定義。
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex_demo.Service import bodyService as bs
from doublex import Stub, ANY_ARG, assert_that, is_
class TestStub(TestCase):
def test_stub(self):
playername = "Kawhi Leonard"
#當(dāng)Stub()不帶參數(shù)的時(shí)候,稱之為Free Stub
#ANY_ARG表示任意參數(shù)
with Stub() as freestub:
freestub.get_assist().returns("6")
freestub.get_score().returns("30")
freestub.get_rebound().returns("8")
freestub.get_match_number(ANY_ARG).returns(playername + " plays 82 games")
player_service_stub_2017 = pls.playerService(playername, 2017, freestub, pos.profileService(playername), bs.bodyService(), ss.salaryService())
#使用freestub代替dataService,來對(duì)待測(cè)對(duì)象playerService進(jìn)行測(cè)試驗(yàn)證
assert_that(player_service_stub_2017.get_player_info().split('\n')[-2], is_("8"))
assert_that(player_service_stub_2017.get_player_info().split('\n')[-1], is_("Kawhi Leonard plays 82 games"))
if __name__ == '__main__':
main()
除了上述的returns()方法外,常用的還有raises()來模擬異常情況
def test_raises(self):
with Stub() as stub:
stub.foo(2).raises(Exception)
with self.assertRaises(Exception):
stub.foo(2)
#stub.foo()的調(diào)用會(huì)發(fā)生異常
如果沒有使用returns()方法,默認(rèn)的返回值為None。
from doublex import Stub
# 如果不是定義返回值,不需要使用with關(guān)鍵字來定義stub
stub = Stub()
stub.foo() #這個(gè)方法會(huì)返回None
#定義返回值,需要使用下面的方式
with Stub() as stub1:
stub1.foo(1).returns("1")
Ad-hoc Stub
通過方法method_returning()和method_raising(),我們可以實(shí)現(xiàn)對(duì)實(shí)例建立stub。這種方法不需要使用Stub()就可以建立stub了。
def test_adhoc_stub(self):
bodyservice = bs.bodyService()
#method_returning()直接在實(shí)例上建立stub,并設(shè)定返回值
bodyservice.get_height = method_returning("210cm")
assert_that(bodyservice.get_height(), is_("210cm"))
#method_raising()直接在實(shí)例上建立stub,并拋出異常
bodyservice.get_weight = method_raising(Exception)
with self.assertRaises(Exception):
bodyservice.get_weight()
Test Spy - Spies remember everything that happens to them
doublex提供了Spy()對(duì)象來創(chuàng)建Test Spy,根據(jù)參數(shù)的不同分為 Spy,Free Spy和Proxy Spy
下面的例子中,我們使用Spy(Collaborator)來代替相應(yīng)的依賴服務(wù),然后驗(yàn)證SUT是否正確的調(diào)用
依賴服務(wù)和傳遞參數(shù)。
Spy
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex import Spy, called, ProxySpy, assert_that
from doublex_demo.Service import bodyService as bs
from doublex_demo.Service import salaryService as ss
class TestSpy(TestCase):
def test_spy(self):
playername = "Kawhi Leonard"
year = 2017
salary = "20m"
#使用Spy(類對(duì)象)來創(chuàng)建spy
spy_ss = Spy(ss.salaryService)
#通過SUT調(diào)用spy對(duì)象的方法
pls.playerService(playername, 2017, ds.dataService(playername), pos.profileService(playername), bs.bodyService(), spy_ss).set_new_salary(salary)
#驗(yàn)證spy_ss.set_salary方法被調(diào)用過
assert_that(spy_ss.set_salary, called())
#Spy是Stub的擴(kuò)展,所以除了記錄方法被調(diào)用的情況,也可以設(shè)定返回值
with Spy(bs.bodyService) as spy_bs_as_stub:
spy_bs_as_stub.get_height().returns("188cm")
spy_bs_as_stub.get_weight().returns("110kg")
spy_bs_as_stub.illnessHistory(2017).returns("Year 2017 no injury")
spy_bs_as_stub.illnessHistory(2018).returns("Year 2017 has ankle injury")
#直接調(diào)用spy對(duì)象方法
spy_bs_as_stub.get_height()
spy_bs_as_stub.get_weight()
spy_bs_as_stub.illnessHistory(2017)
spy_bs_as_stub.illnessHistory(2018)
#可以驗(yàn)證spy對(duì)象方法已經(jīng)被調(diào)用及其參數(shù)接受情況
assert_that(spy_bs_as_stub.get_height, called())
assert_that(spy_bs_as_stub.get_weight, called())
assert_that(spy_bs_as_stub.illnessHistory, called().times(2))
#使用anything()去任意匹配
assert_that(spy_bs_as_stub.illnessHistory, called().with_args(anything())) #通過SUT調(diào)用spy對(duì)象的方法
player_service_spy_2016 = pls.playerService(playername, 2017, ds.dataService(playername), pos.profileService(playername), spy_bs_as_stub, ss.salaryService())
player_service_spy_2016.get_physical_feature(2017)
#驗(yàn)證spy對(duì)象方法再一次被方法(SUT)調(diào)用
assert_that(spy_bs_as_stub.get_height, called().times(2))
assert_that(spy_bs_as_stub.get_weight, called().times(2))
assert_that(spy_bs_as_stub.illnessHistory, called().times(3))
if __name__ == '__main__':
main()
Free Spy
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex import Spy, called, ProxySpy, assert_that
from doublex_demo.Service import bodyService as bs
from doublex_demo.Service import salaryService as ss
class TestSpy(TestCase):
def test_spy(self):
playername = "Kawhi Leonard"
year = 2017
salary = "20m"
#使用with關(guān)鍵字和Spy()來創(chuàng)建free spy
#設(shè)置和salaryService一樣的方法
with Spy() as free_ss_spy:
free_ss_spy.set_salary(salary).returns("20m")
#通過SUT調(diào)用spy對(duì)象的方法
pls.playerService(playername, 2017, ds.dataService(playername), pos.profileService(playername), bs.bodyService(), free_ss_spy).set_new_salary(salary)
#驗(yàn)證spy_ss.set_salary方法被調(diào)用過
assert_that(free_ss_spy.set_salary, called())
if __name__ == '__main__':
main()
ProxySpy
ProxySpy()與Spy()不同的地方是ProxySpy()接受的對(duì)象是實(shí)例。
不過我們要注意盡量不要使用ProxySpy(),官方文檔給出了如下解釋:
Note the ProxySpy breaks isolation. It is not really a double. Therefore is always the worst double and the last resource.
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex import Spy, called, ProxySpy, assert_that
from doublex_demo.Service import bodyService as bs
from doublex_demo.Service import salaryService as ss
class TestSpy(TestCase):
def test_spy(self):
playername = "Kawhi Leonard"
year = 2017
salary = "20m"
#傳遞實(shí)例給ProxySpy()
spy_pos = ProxySpy(pos.profileService(playername))
#通過SUT調(diào)用spy對(duì)象的方法
pls.playerService(playername, 2016, ds.dataService(playername), spy_pos, bs.bodyService(), ss.salaryService()).get_player_info()
#驗(yàn)證spy對(duì)象方法被調(diào)用過
assert_that(spy_pos.get_player_team, called())
if __name__ == '__main__':
main()
Spy的驗(yàn)證最常用的是下面兩個(gè)方法
-
called()驗(yàn)證方法調(diào)用情況 -
with_args()驗(yàn)證參數(shù)調(diào)用情況
一個(gè)典型的例子如下
from hamcrest import contains_string, less_than, greater_than
from doublex import Spy, assert_that, called
#不設(shè)置返回值,可以不用with關(guān)鍵字
spy = Spy()
spy.m1()
spy.m2(None)
spy.m3(2)
spy.m4("hi", 3.0)
spy.m5([1, 2])
spy.m6(name="john doe")
assert_that(spy.m1, called())
assert_that(spy.m2, called())
assert_that(spy.m1, called().with_args())
assert_that(spy.m2, called().with_args(None))
assert_that(spy.m3, called().with_args(2))
assert_that(spy.m4, called().with_args("hi", 3.0))
assert_that(spy.m5, called().with_args([1, 2]))
assert_that(spy.m6, called().with_args(name="john doe"))
#使用hamcrest matchers豐富判斷條件
assert_that(spy.m3, called().with_args(less_than(3)))
assert_that(spy.m3, called().with_args(greater_than(1)))
assert_that(spy.m6, called().with_args(name=contains_string("doe")))
calls()方法可以幫助我們獲得更加具體的參數(shù)值和返回值,從而進(jìn)行復(fù)雜的驗(yàn)證
def test_calls_spy(self):
salary = "20m"
year = 2017
#創(chuàng)建spy
with Spy(ss.salaryService) as ss_spy:
ss_spy.set_salary(salary)
#調(diào)用方法
ss_spy.set_salary(salary)
ss_spy.set_salary("22m")
#使用calls取得調(diào)用傳入的參數(shù)
#多次調(diào)用可以多次取得,calls是一個(gè)數(shù)組
assert_that(ss_spy.set_salary.calls[0].args, is_((salary, )))
assert_that(ss_spy.set_salary.calls[1].args, is_(("22m", )))
#創(chuàng)建spy
with Spy(bs.bodyService) as bs_spy:
bs_spy.get_height().returns("190cm")
bs_spy.illnessHistory(year).returns("no injury")
#調(diào)用方法
bs_spy.get_height()
bs_spy.illnessHistory(year)
#使用calls取得調(diào)用傳入的參數(shù)和返回值
assert_that(bs_spy.get_height.calls[0].retval, is_("190cm"))
assert_that(bs_spy.illnessHistory.calls[0].args, is_((year, )))
assert_that(bs_spy.illnessHistory.calls[0].retval, is_("no injury"))
Mock Object - Mock forces the predefined script
doublex提供了Mock()來實(shí)現(xiàn)Mock Object的創(chuàng)建。Mock Object預(yù)先定義了一些方法調(diào)用的順序,然后Mock Object被調(diào)用的時(shí)候,會(huì)去驗(yàn)證方法是否按照預(yù)定義的順序和參數(shù)調(diào)用。驗(yàn)證很簡單地使用doublex.verify()方法,如果不去驗(yàn)證調(diào)用的順序,可以使用doublex.any_order_verify()。
from unittest import TestCase, main
from doublex_demo.Service import dataService as ds
from doublex_demo.Service import profileService as pos
from doublex_demo.Service import playerService as pls
from doublex import Mock, verify, assert_that, any_order_verify
from doublex_demo.Service import bodyService as bs
from doublex_demo.Service import salaryService as ss
class TestSpy(TestCase):
def test_spy(self):
playername = "Kawhi Leonard"
year = 2017
salary = "20m"
#使用with關(guān)鍵字和Mock()創(chuàng)建mock object
#假設(shè)替代salaryService對(duì)象
#定義mock需要調(diào)用的方法及其參數(shù),此方法與被替代的salaryService中的方法相同
with Mock() as mock:
mock.set_salary("20m")
#在SUT playerservice中調(diào)用這個(gè)mock
#之前定義的mock.set_salary("20m")會(huì)被SUT調(diào)用
player_service_mock_2017 = pls.playerService(playername, year, ds.dataService(playername), pos.profileService(playername), bs.bodyService(), mock)
player_service_mock_2017.set_new_salary(salary)
#verify()驗(yàn)證定義的mock期望是否正確被實(shí)現(xiàn)
assert_that(mock, verify())
#假設(shè)替代dataService對(duì)象
#mock可以設(shè)置返回值
with Mock() as mock_order:
mock_order.get_score().returns("22")
mock_order.get_assist().returns("3")
mock_order.get_rebound().returns("6")
mock_order.get_match_number(year).returns("77")
#在SUT playerservice中調(diào)用這個(gè)mock
player_service_mock_2017_order = pls.playerService(playername, year, mock_order, pos.profileService(playername), bs.bodyService(), ss.salaryService())
player_service_mock_2017_order.get_player_info()
# verify()驗(yàn)證定義的mock期望是否正確被實(shí)現(xiàn),且方法調(diào)用順序必須完全一致
assert_that(mock_order, verify())
#假設(shè)替代dataService對(duì)象,注意mock定義中期望的順序和之前不一樣,也會(huì)和執(zhí)行順序不一致
with Mock() as mock_any_order:
mock_any_order.get_score().returns("22")
mock_any_order.get_rebound().returns("6")
mock_any_order.get_match_number(year).returns("77")
mock_any_order.get_assist().returns("3")
#在SUT playerservice中調(diào)用這個(gè)mock
player_service_mock_2017_any_order = pls.playerService(playername, year, mock_any_order, pos.profileService(playername),
bs.bodyService(), ss.salaryService())
player_service_mock_2017_any_order.get_player_info()
#any_order_verify()驗(yàn)證定義的mock期望是否正確被實(shí)現(xiàn),且方法調(diào)用順序不要求完全一致
assert_that(mock_any_order, any_order_verify())
if __name__ == '__main__':
main()
Stubbing properties
對(duì)于使用@property的類,doublex提供了非常簡單的處理
#假設(shè)Student是我們需要替換的類
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
from doublex import Spy, Mock, Stub, assert_that, is_
from doublex import property_got, property_set, never, verify
#創(chuàng)建stub,可以直接復(fù)制,相當(dāng)于set操作
with Stub(Student) as stub:
stub.score = 88
assert_that(stub.score, is_(88))
#建立spy
spy = Spy(Student)
#讀取score屬性,由于沒有設(shè)置返回值,這里的value會(huì)是None
value = spy.score
#使用property_got()方法驗(yàn)證屬性是否被調(diào)用
assert_that(spy, property_got('score'))
spy.score = 59
spy.score = 98
spy.score = 98
#使用property_set()方法驗(yàn)證賦值情況
assert_that(spy, property_set('score').to(59))
assert_that(spy, property_set('score').to(98).times(2))
assert_that(spy, never(property_set('score').to(99)))
#創(chuàng)建mock
with Mock(Student) as mock:
#設(shè)定期望為調(diào)用score
mock.score
#調(diào)用score
mock.score
#verify()驗(yàn)證
assert_that(mock, verify())
#創(chuàng)建mock
with Mock(Student) as mock1:
#設(shè)定期望為score被賦值為80
mock1.score = 80
#賦值80
mock1.score=80
#verify()驗(yàn)證
assert_that(mock1, verify())
Stub delegates
對(duì)于Stub返回值的設(shè)定,除了returns()方法,doublex還提供了一個(gè)delegates()方法。delegates()方法接受函數(shù)或生成器或其他可迭代的對(duì)象為參數(shù)。
def test_delegate_stub(self):
def get_height():
return "181cm"
#創(chuàng)建stub
with Stub(bs.bodyService) as stub:
#使用delegates()來設(shè)定返回值,接受方法或是可以迭代的對(duì)象
stub.get_height().delegates(get_height)
stub.get_weight().delegates(["120kg", "121kg"])
#驗(yàn)證返回值
assert_that(stub.get_height(), is_("181cm"))
assert_that(stub.get_weight(), is_("120kg"))
assert_that(stub.get_weight(), is_("121kg"))
Stub observer
Stub的方法是可以被觀察的??梢允褂?code>attach()方法把一個(gè)任意方法和Stub方法綁定起來,然后在每次Stub方法調(diào)用的時(shí)候,這個(gè)attached的方法也會(huì)被調(diào)用。這樣的話,我們就可以在Stub中執(zhí)行其他代碼。
def test_observer_stub(self):
def bar():
print("I am attached")
with Stub() as stub:
stub.foo().returns("I am foo")
stub.foo.attach(bar)
#bar()會(huì)在這里執(zhí)行
assert_that(stub.foo(), is_("I am foo"))
Inline stubbing and mocking
doublex創(chuàng)建mock/stub/spy一般使用的是double context manager的方式,語法如下所示
from doublex import Stub
with Stub() as stub:
stub.method(<args>).returns(<value>)
為了易讀性,doublex還提供了when()和expect_all()來實(shí)現(xiàn)同樣的創(chuàng)建功能。
when()用于stub和spy
def test_inline_stub(self):
#Stub()創(chuàng)建free stub
inline_stub_free = Stub()
#使用when()設(shè)置方法參數(shù)和返回值
when(inline_stub_free).foo(1).returns("I am inline free stub")
assert_that(inline_stub_free.foo(1), is_("I am inline free stub"))
#Stub(Collaborator)創(chuàng)建stub
inline_stub = Stub(bs.bodyService)
# 使用when()設(shè)置方法參數(shù)和返回值
when(inline_stub).get_height().returns("188cm")
assert_that(inline_stub.get_height(), is_("188cm"))
def test_inline_spy(self):
#Spy()創(chuàng)建free spy
spy_inline_free = Spy()
#使用when()設(shè)置方法參數(shù)和返回值
when(spy_inline_free).foo().returns("I am inline foo")
#調(diào)用方法
spy_inline_free.foo()
#驗(yàn)證調(diào)用情況
assert_that(spy_inline_free.foo(), is_("I am inline foo"))
assert_that(spy_inline_free.foo, called())
#Spy()創(chuàng)建spy
spy_inline = Spy(ss.salaryService)
#使用when()設(shè)置方法參數(shù)
when(spy_inline).set_salary(ANY_ARG)
#調(diào)用方法
spy_inline.set_salary("12m")
#驗(yàn)證調(diào)用情況
assert_that(spy_inline.set_salary, called().with_args("12m"))
expect_all()用于mock
def test_inline_mock(self):
playername = "Kawhi Leonard"
year = 2017
#使用Mock()創(chuàng)建mock
inline_mock = Mock()
#使用expect_all()去設(shè)置期望值
expect_call(inline_mock).get_score().returns("33")
expect_call(inline_mock).get_assist().returns("6")
expect_call(inline_mock).get_rebound().returns("7")
expect_call(inline_mock).get_match_number(year).returns("no injury")
#在SUT playerservice中調(diào)用這個(gè)mock
player_service_mock_2017_order = pls.playerService(playername, year, inline_mock, pos.profileService(playername), bs.bodyService(), ss.salaryService())
player_service_mock_2017_order.get_player_info()
# verify()驗(yàn)證定義的mock期望是否正確被實(shí)現(xiàn),且方法調(diào)用順序必須完全一致
assert_that(inline_mock, verify())
Asynchronous spies
有些情況下SUT調(diào)用依賴組件是一個(gè)異步行為,有可能依賴組件的調(diào)用執(zhí)行是延后的,這樣的話就會(huì)產(chǎn)生下面的問題
# THE WRONG WAY
class AsyncTests(unittest.TestCase):
def test_wrong_try_to_test_an_async_invocation(self):
# given
spy = Spy(Collaborator)
sut = SUT(spy)
# when
sut.some_method()
# then
assert_that(spy.write, called())
上面代碼中,called()的驗(yàn)證是有可能在spy.write()執(zhí)行之前就進(jìn)行了。
doublex提供了一個(gè)called.async(timeout) matcher來支持異步的spy驗(yàn)證
The called assertion waits the corresponding invocation a maximum of timeout seconds.
# THE DOUBLEX WAY
class AsyncTests(unittest.TestCase):
def test_test_an_async_invocation_with_doublex_async(self):
# given
spy = Spy(Collaborator)
sut = SUT(spy)
# when
sut.some_method()
# then
assert_that(spy.write, called().async(timeout=1))