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