今天這一章還是講測試,基于屬性的測試(Property-Based Testing)。其實(shí)我感覺叫隨機(jī)驗(yàn)證測試更通俗易懂一點(diǎn)。
我們?nèi)粘W约簻y試的時(shí)候,會(huì)有一個(gè)問題,就是我們既是裁判,也是球員。因?yàn)槲覀兊某绦蚓褪琼樦覀兊乃悸穼懙模瑴y試也是按照同樣的思路來測試的。所以,我們可能通常很難測出我們自己寫出來的bug。
解決這個(gè)問題的一個(gè)辦法,就是把測試交給其他人。這也是測試這個(gè)崗位存在的意義之一。但是,如果我們把測試交給了其他人,上一章提到的,對測試的思考可以幫助更好地設(shè)計(jì)程序,這個(gè)好處就不存在了。
解決這個(gè)問題的辦法就是,把其他人,換成我們的計(jì)算機(jī),讓計(jì)算機(jī)來幫我們自動(dòng)測試。
之前關(guān)于契約或者合約(Contract)那一章中,提到了要給程序制定一個(gè)合同,規(guī)定了輸入的規(guī)則、輸出的規(guī)則、以及不變量(比如給一個(gè)數(shù)組排序,進(jìn)去和出來的數(shù)組長度應(yīng)該是不變的)。
那么合約和不變量在這里統(tǒng)稱為屬性(property),很抽象是不是?個(gè)人覺得它的定義并不重要,舉個(gè)例子就懂了。
比如我們要驗(yàn)證一個(gè)list的排序,我們可以驗(yàn)證這兩件事:1.輸入和輸出的list長度是否相等;2.是不是list里的每一個(gè)都比它前面一個(gè)大。
用python寫出來就是這樣:
from hypothesis import given
import hypothesis.strategies as some
@given(some.lists(some.Integers()))
def test_list_size_is_invariant_across_sorting(a_list):
original_length = len(a_list)
a_list.sort()
assert len(a_list) == original_length
@given(some.lists(some.text()))
def test_sorted_result_is_ordered(a_list):
a_list.sort()
for i in range(len(a_list) – 1):
assert a_list[i] <= a_list[i + 1]
更重要的是,它利用了hypothesis這個(gè)模塊,@given(some.lists(some.integers()))會(huì)讓它在運(yùn)行的時(shí)候,利用隨機(jī)的數(shù)值把同樣的方法運(yùn)行100次。它會(huì)把出現(xiàn)錯(cuò)誤的情況記錄下來。
之前在學(xué)習(xí)算法的時(shí)候,也接觸到了一個(gè)叫做對數(shù)器的概念,其實(shí)和這里的基于屬性測試,基本上是一件事情。
就是為了驗(yàn)證我們的算法A寫對了,我們先寫一個(gè)絕對正確的算法B,不管效率,只管正確。然后用同樣的數(shù)據(jù)同時(shí)跑算法A和算法B。當(dāng)然也是隨機(jī)跑很多次啦。如果兩個(gè)出現(xiàn)了不同的結(jié)果,那就說明算法A寫錯(cuò)了。
思路基本相同,只不過,相對來說,可能這個(gè)基于屬性測試更嚴(yán)謹(jǐn)一點(diǎn),比如,同樣是排序算法,我用我寫的冒泡排序算法來驗(yàn)證我的選擇排序算法,萬一,我的冒泡排序也寫錯(cuò)了呢?但是,如果我直接從根本上解析出排序(正序)就是后一個(gè)比前一個(gè)大,顯然是更不容易出問題的,其實(shí)也能更進(jìn)一步地鍛煉我們尋找根本問題的能力。
當(dāng)然啦,嚴(yán)格意義上來說,比較后一個(gè)比前一個(gè)大,這個(gè)本身也是一種算法。
實(shí)話說,要思考清楚有哪些屬性是要測試的,這件事本身就充滿了難度。如果你平時(shí)沒有這個(gè)習(xí)慣,突然讓你想,你會(huì)大腦一片空白的,就像是剛學(xué)編程那會(huì),遇到了一個(gè)需求完全不知道從哪里下手的那種感覺。坦白的說,我現(xiàn)在就是這種狀態(tài),想必這也是需要刻意練習(xí)的。
這種隨機(jī)大量測試的方式,可以幫助我們測出一些邊界值,測出一些我們想不到的情況。往往最容易出問題的地方也是在邊界值的地方。就跟開車似的,車開起來了,通常都沒什么問題,但是起步可能會(huì)熄火,停車可能會(huì)倒不進(jìn)去。
關(guān)于這個(gè)基于屬性測試的框架,我隨手搜了一搜,Python有的,沒理由咱們Java沒有,對不對?
一、找到兩個(gè)github上開源:
1.https://github.com/HypothesisWorks/hypothesis-java
2.https://github.com/quicktheories/QuickTheories
二、找到一個(gè)都已經(jīng)有自己網(wǎng)站的(雖然也有g(shù)ithub):
https://jqwik.net/
三、還有一個(gè)直接是JUnit家的JUnit-Quickcheck(感覺上這個(gè)更香一點(diǎn))
https://github.com/pholser/junit-quickcheck
我還沒來得及仔細(xì)研究,朋友們可以先自行研究起來~
書中其實(shí)還提到了另外一個(gè)更加實(shí)際一點(diǎn)的例子,但我個(gè)人覺得那算不上bug,它和實(shí)際的需求有關(guān)系。這邊簡單提一下吧,或許你也有不同的見解。
大概就是有一個(gè)倉庫類,它可以存放各種貨品,不同貨品有各自的數(shù)量,大概是這樣一個(gè)結(jié)構(gòu)吧:List<Map<String,Integer>>。然后我們可以查詢某個(gè)貨品是否有庫存、查詢某個(gè)貨品還剩多少庫存、可以從中取出某個(gè)數(shù)量的貨品。
代碼如下:
class Warehouse:
def __init__(self, stock):
self.stock = stock
def in_stock(self, item_name):
return (item_name in self.stock) and (self.stock[item_name] > 0)
def take_from_stock(self, item_name, quantity):
if quantity <= self.stock[item_name]:
self.stock[item_name] -= quantity
else:
raise Exception("Oversold {}".format(item_name))
def stock_count(self, item_name):
return self.stock[item_name]
倉庫的初始化是這樣的:
wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0})
然后,同樣用了hypothesis來做批量測試,然后在調(diào)用take_from_stock( item_name='hats', quantity=3)這樣一組數(shù)據(jù)的時(shí)候報(bào)錯(cuò)了。
作者說,在in_stock我們不應(yīng)該只判斷庫存是不是大于0,而是要判斷它有沒有包含我們要拿取的貨品數(shù)。代碼應(yīng)該改成這樣:
def in_stock(self, item_name, quantity):
return (item_name in self.stock) and (self.stock[item_name] >= quantity)
反正我是覺得這不算個(gè)bug吧,畢竟在真正獲取貨品的時(shí)候,就報(bào)錯(cuò)了呀。要看我們對于in_stock這個(gè)方法本身的要怎么定義咯,是只需要知道它還有沒有庫存,還是需要知道它有沒有我需要的庫存。
雖然實(shí)際需求中,后者可能性更大,但是在take_from_stock方法里報(bào)錯(cuò),又有什么問題呢?(又或者,作者只是想舉個(gè)例子,是我太較真了)
基于屬性的測試是對于單元測試的補(bǔ)充,對于單元測試的思考,可以讓我們思考代碼實(shí)現(xiàn)的其他方式?;趯傩缘臏y試,可以讓我們更加清晰,我們的方法能干什么不能干什么,同時(shí),也消除一些意外的情況。
如果,你還沒有把這兩種測試用起來,現(xiàn)在就趕緊用起來吧~
]]>