我想嘗試測驗驅動的開發,但我正在進行的專案涉及很多隨機性,我非常不確定如何測驗它。這是我可能想要撰寫的那種演算法的玩具示例:
撰寫一個不帶引數并回傳滿足以下屬性的隨機整數串列的函式
- 每個整數都在 0 到 10 之間
- 相同的數字不會出現兩次
- 該串列的長度為 3 90% 的時間,長度為 4 的時間為 10%
- 數字 3 有 50% 的機會出現
我不需要測驗確切的統計分布,但顯然我希望如果有人完全洗掉相應的代碼,測驗將會失敗。
我正在使用一個您可以假設是正確的外部 RNG,并且我在如何構建代碼方面非常自由,因此我可以使用依賴注入來讓測驗使用假 RNG,但我仍然不明白那是怎么回事有助于。例如,即使我總是使用相同的種子進行測驗,一旦我重構演算法以以不同的順序選擇亂數,所有測驗都變得毫無意義。
我想前兩點可以通過生成許多案例并檢查約束是否得到滿足來進行測驗,但這并不像 TDD。
對于最后兩點,我正在考慮使用不同的配置進行測驗,例如 90% 是 100% 或 0%,然后我可以測驗串列的長度是否確實是 3 或 4。我猜它會起作用,但它似乎有點弱。
在使用 TDD 測驗涉及隨機性的演算法時,是否有任何指南或其他技術可供使用?
uj5u.com熱心網友回復:
有幾種方法可以解決這樣的問題,我將來可能會添加另一個答案,但我立即發現最引人注目的方法是將測驗驅動開發 (TDD) 與基于屬性的測驗相結合。
您可以使用多種語言和各種框架來執行此操作。在這里,我將使用原始的基于屬性的測驗庫
您如何可靠地區分這種實作和“真實”亂數生成器,后者恰好在更改為其他數字之前發出有限的 4 序列?
因此,我們可以在實際上不表達所有約束的穩定測驗或偶爾報告錯誤結果的更精確測驗之間進行選擇。
這里的一種設計方法是傾向于“可測驗性”——在我們介面的外觀背后,是一種將通用隨機位源與將位序列映射到某個結果的確定性函式相結合的實作。
def randomListOfIntegers():
seed_bits = generalPurpose.random_bits()
return determisticFunction(seed_bits)
def deterministicFunction(seed_bits):
???
聲稱randomListOfIntegers“如此簡單,顯然沒有缺陷”,因此我們可以通過檢查來確定其正確性,并集中精力進行設計deterministicFunction。
現在,我們遇到了第二個問題:seed_bits 到某個可觀察結果的映射是任意的。大多數業務領域問題(例如:工資單系統)對于任何給定的輸入都有一個單一的預期輸出,但在隨機系統中你仍然有一些額外的自由度。如果您撰寫的函式在給定任何位序列的情況下產生可接受的答案,那么我的函式(將位反轉然后呼叫您的函式)也將產生可接受的答案——即使我的答案和您的答案不同。
實際上,如果我們想要一套測驗在代碼更改導致行為變化時發出警報,那么我們必須發明我們想要鎖定的行為規范。
除非我們對哪些任意行為將支持干凈的實作有一個很好的猜測,否則這可能會非常痛苦。
(或者,我們只是依靠我們的“驗收”測驗池,它忽略了切換到不同任意行為的代碼更改——它一直在進行權衡)。
我們可能考慮的更簡單的實作之一是將 seed_bits 視為候選回應序列的索引
def deterministicFunction(seed_bits):
choices = ???
n = computeIndex(seed_bits, len(choices))
return choices[n]
這暴露了另一個問題:k seed_bits 意味著 2^k 自由度;除非len(choices)碰巧是 2 的冪且不大于 2^k,否則選擇時會有一些偏差。您可以通過為 k 選擇足夠大的值來使偏差誤差任意小,但您不能用有限的位數來消除它。
進一步分解問題,我們可以將作業分成兩個元素,一個負責生成候選池,另一個負責實際選擇其中一個。
def deterministicFunction(seed_bits):
return choose(seed_bits, weighted_candidates())
def choose(seed_bits, weighted_candidates):
choices = []
# note: the order of elements in this enumeration
# is still arbitrary
for candidate, weight in weighted_candidates:
for _ in range(weight):
choices.add(candidate)
# technically, this is also still arbirary
n = computeIndex(seed_bits, len(choices))
return choices[n]
此時,我們可以決定使用“最簡單的可能可行的東西”來實作computeIndex(先測驗,如果你愿意的話),而且這個新weighted_candidates()功能也很容易測驗,因為它的每個測驗只是“計算候選人和確保整個人口都滿足問題的限制條件”。 choose可以使用更簡單的總體作為輸入進行測驗。
這種實作可能不能令人滿意——畢竟,我們正在構建這種候選資料結構,然后是另一個選擇,只選擇一個。這可能是我們能做的最好的了。然而,通常不同的實作是可能的。
實際上,問題規范為我們定義了(加權)回應群體的大小。換句話說,len(choices)確實是一些常數L。
choices = [ generate(n) for n in range(L)]
n = computeIndex(seed_bits, L)
return choices[n]
這又可以簡化為
n = computeIndex(seed_bits, L)
return generate(n)
也就是說,如果我們可以計算出哪個回應在第 n 位,我們就不需要傳遞一堆資料結構。
請注意,雖然generate(n)仍然具有任意行為,但我們可以對資料結構做出明確的斷言[generate(n) for n in range(L)]。
重構一點來清理東西,我們可能有
def randomListOfIntegers():
seed_bits = generalPurpose.random_bits()
n = computeIndex(seed_bits, L)
return generateListOfIntegers(n)
請注意,這個骨架并不是從撰寫一堆測驗和重構中“出現”的,而是來自對問題和我們需要考慮的選擇的思考,以“控制決策和反饋之間的差距”。
將其稱為“尖峰”可能是公平的——我們使用它來更好地理解我們正在嘗試解決的問題的沙盒練習。
相同的數字不會出現兩次
對組合學的認識在這里會有所幫助。
基本思想:我們可以計算集合 [0,1,2,3,4,5,6,7,8,9,10] 的 4 個唯一元素的所有可能排列的集合,我們可以使用一種稱為壓縮排序以生成其中的特定子集。
在這里,我們可能想要3更仔細地處理特殊情況。粗糙的骨架看起來像
def generateListOfIntegers(n):
other_numbers = [0,1,2,4,5,6,7,8,9,10]
has3, hasLength3, k = decode(n)
if has3:
if hasLength3:
# 45 distinct candidates
assert 0 <= k < 45
return [3] choose(other_numbers, 2, k)
else:
# 120 distinct candidates
assert 0 <= k < 120
return [3] choose(other_numbers, 3, k)
else:
if hasLength3:
# 120 distinct candidates
assert 0 <= k < 120
return choose(other_numbers, 3, k)
else:
# 210 distinct candidates
assert 0<= k < 210
return choose(other_numbers, 4, k)
Wherechoose(other_numbers, j, k)回傳包含總元素kth的 other_numbers 的子集,并具有確保總體權重正確的邏輯。jdecode(n)
的行為choose是任意的,但是子集的行程有一個“自然”的順序(即,您可以“排序”它們),因此任意使用排序順序是合理的。
可能還值得注意的是,這choose是非常通用的——我們傳入的串列幾乎可以是任何東西,它真的不關心你對答案做了什么。與之相比decode,我們對“正確”行為的定義與它的消費緊密相關generateListOfNumbers。
您可能想查看 Peter Seiber 的Fischer Chess 練習,了解當 TDD 新出現時人們采用的不同方法。警告,執行緒現在被嚴重破壞了,所以你可能需要篩選多個執行緒才能找到所有好的部分。
uj5u.com熱心網友回復:
首先,TDD 的方法不止一種,因此沒有唯一的正確答案。但這是我對此的看法:
您提到您不需要測驗確切的統計分布,但我認為您必須。否則,撰寫滿足您的測驗的最簡單的代碼將導致完全確定的、非隨機的解決方案。(如果您在不考慮隨機性的情況下查看您的要求,您會發現您可以使用一個簡單的回圈和幾個 if 陳述句來滿足它們)。但顯然,這不是你真正想要的。因此,在您的情況下,您必須撰寫一個測驗來檢查演算法的統計分布。
此類測驗需要收集您在測驗中的功能的許多結果,因此可能會很慢,因此有些人會認為這是一種不好的做法,但恕我直言,這是您實際測驗您真正關心的內容的唯一方法。
假設這不僅僅是一個理論練習,您可能還希望將結果保存到一個檔案中,以便以后手動檢查(例如使用 Excel),檢查結果的其他統計屬性,并可能相應地添加或調整您的測驗。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/475368.html
