|
|
|
|
@@ -21,6 +21,10 @@
|
|
|
|
|
<link rel="stylesheet" href="../gitbook/style.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="../gitbook/plugins/gitbook-plugin-katex/katex.min.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="../gitbook/plugins/gitbook-plugin-highlight/website.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -44,7 +48,7 @@
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="book" data-level="11.2" data-chapter-title="測試函數" data-filepath="ch11/ch11-02.md" data-basepath=".." data-revision="Fri Dec 25 2015 12:32:44 GMT+0800 (中国标准时间)">
|
|
|
|
|
<div class="book" data-level="11.2" data-chapter-title="測試函數" data-filepath="ch11/ch11-02.md" data-basepath=".." data-revision="Mon Dec 28 2015 16:03:52 GMT+0800 (中国标准时间)">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="book-summary">
|
|
|
|
|
@@ -238,7 +242,7 @@
|
|
|
|
|
|
|
|
|
|
<b>1.5.</b>
|
|
|
|
|
|
|
|
|
|
穫取URL
|
|
|
|
|
獲取URL
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -253,7 +257,7 @@
|
|
|
|
|
|
|
|
|
|
<b>1.6.</b>
|
|
|
|
|
|
|
|
|
|
併發穫取多個URL
|
|
|
|
|
併發獲取多個URL
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -802,7 +806,7 @@
|
|
|
|
|
|
|
|
|
|
<b>5.10.</b>
|
|
|
|
|
|
|
|
|
|
Recover捕穫異常
|
|
|
|
|
Recover捕獲異常
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1315,7 +1319,7 @@
|
|
|
|
|
|
|
|
|
|
<b>8.9.</b>
|
|
|
|
|
|
|
|
|
|
併發的退齣
|
|
|
|
|
併發的退出
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1834,7 +1838,7 @@
|
|
|
|
|
|
|
|
|
|
<b>12.7.</b>
|
|
|
|
|
|
|
|
|
|
穫取結構體字段標識
|
|
|
|
|
獲取結構體字段標識
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2046,7 +2050,7 @@
|
|
|
|
|
<span class="hljs-keyword">return</span> <span class="hljs-constant">true</span>
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>在相同的目録下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩個測試函數. 每一個都是測試 IsPalindrome 是否給齣正確的結果, 併使用 t.Error 報告失敗:</p>
|
|
|
|
|
<p>在相同的目録下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩個測試函數. 每一個都是測試 IsPalindrome 是否給出正確的結果, 併使用 t.Error 報告失敗:</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> word
|
|
|
|
|
|
|
|
|
|
<span class="hljs-keyword">import</span> <span class="hljs-string">"testing"</span>
|
|
|
|
|
@@ -2070,7 +2074,7 @@
|
|
|
|
|
<pre><code>$ cd $GOPATH/src/gopl.io/ch11/word1
|
|
|
|
|
$ go test
|
|
|
|
|
ok gopl.io/ch11/word1 0.008s
|
|
|
|
|
</code></pre><p>還比較滿意, 我們運行了這個程序, 不過沒有提前退齣是因爲還沒有遇到BUG報告. 一個法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識别 ‘‘été.’’. 另外一個來自美國中部用戶的抱怨是不能識别 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.</p>
|
|
|
|
|
</code></pre><p>還比較滿意, 我們運行了這個程序, 不過沒有提前退出是因爲還沒有遇到BUG報告. 一個法国名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識别 ‘‘été.’’. 另外一個來自美国中部用戶的抱怨是不能識别 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">func</span> TestFrenchPalindrome(t *testing.T) {
|
|
|
|
|
<span class="hljs-keyword">if</span> !IsPalindrome(<span class="hljs-string">"été"</span>) {
|
|
|
|
|
t.Error(<span class="hljs-string">`IsPalindrome("été") = false`</span>)
|
|
|
|
|
@@ -2093,7 +2097,7 @@ ok gopl.io/ch11/word1 0.008s
|
|
|
|
|
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
|
|
|
|
|
FAIL
|
|
|
|
|
FAIL gopl.io/ch11/word1 0.014s
|
|
|
|
|
</code></pre><p>先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題.</p>
|
|
|
|
|
</code></pre><p>先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們才能定位我們要眞正解決的問題.</p>
|
|
|
|
|
<p>先寫測試用例的另好處是, 運行測試通常會比手工描述報告的處理更快, 這讓我們可以進行快速地迭代. 如果測試集有很多運行緩慢的測試, 我們可以通過隻選擇運行某些特定的測試來加快測試速度.</p>
|
|
|
|
|
<p>參數 <code>-v</code> 用於打印每個測試函數的名字和運行時間:</p>
|
|
|
|
|
<pre><code>$ go test -v
|
|
|
|
|
@@ -2110,7 +2114,7 @@ FAIL gopl.io/ch11/word1 0.014s
|
|
|
|
|
FAIL
|
|
|
|
|
exit status 1
|
|
|
|
|
FAIL gopl.io/ch11/word1 0.017s
|
|
|
|
|
</code></pre><p>參數 <code>-run</code> 是一個正則表達式, 隻有測試函數名被它正確匹配的測試函數纔會被 <code>go test</code> 運行:</p>
|
|
|
|
|
</code></pre><p>參數 <code>-run</code> 是一個正則表達式, 隻有測試函數名被它正確匹配的測試函數才會被 <code>go test</code> 運行:</p>
|
|
|
|
|
<pre><code>$ go test -v -run="French|Canal"
|
|
|
|
|
=== RUN TestFrenchPalindrome
|
|
|
|
|
--- FAIL: TestFrenchPalindrome (0.00s)
|
|
|
|
|
@@ -2178,14 +2182,14 @@ FAIL gopl.io/ch11/word1 0.014s
|
|
|
|
|
<pre><code>$ go test gopl.io/ch11/word2
|
|
|
|
|
ok gopl.io/ch11/word2 0.015s
|
|
|
|
|
</code></pre><p>這種表格驅動的測試在Go中很常見的. 我們很容易想表格添加新的測試數據, 併且後面的測試邏輯也沒有冗餘, 這樣我們可以更好地完善錯誤信息.</p>
|
|
|
|
|
<p>失敗的測試的輸齣併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息.</p>
|
|
|
|
|
<p>失敗的測試的輸出併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息.</p>
|
|
|
|
|
<p>如果我們眞的需要停止測試, 或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因, 我們可以使用 t.Fatal 或 t.Fatalf 停止測試. 它們必鬚在和測試函數同一個 goroutine 內調用.</p>
|
|
|
|
|
<p>測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸齣, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴布爾類型的函數時, 可以忽略併沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸齣一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.</p>
|
|
|
|
|
<p>測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸出, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴布爾類型的函數時, 可以忽略併沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸出一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.</p>
|
|
|
|
|
<p><strong>練習 11.1:</strong> 爲 4.3節 中的 charcount 程序編寫測試.</p>
|
|
|
|
|
<p><strong>練習 11.2:</strong> 爲 (§6.5)的 IntSet 編寫一組測試, 用於檢査每個操作後的行爲和基於內置 map 的集合等價 , 後面 練習11.7 將會用到.</p>
|
|
|
|
|
<h3 id="1121-隨機測試">11.2.1. 隨機測試</h3>
|
|
|
|
|
<p>表格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣泛的隨機輸入來測試探索函數的行爲.</p>
|
|
|
|
|
<p>那麽對於一個隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式.</p>
|
|
|
|
|
<p>那麽對於一個隨機的輸入, 我們如何能知道希望的輸出結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸出結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸出的模式.</p>
|
|
|
|
|
<p>下面的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串.</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">import</span> <span class="hljs-string">"math/rand"</span>
|
|
|
|
|
|
|
|
|
|
@@ -2217,7 +2221,7 @@ ok gopl.io/ch11/word2 0.015s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌穫取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更複雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記録隨機數種子卽可(像上面的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試.</p>
|
|
|
|
|
<p>雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌獲取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更複雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記録隨機數種子卽可(像上面的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試.</p>
|
|
|
|
|
<p>通過使用當前時間作爲隨機種子, 在整個過程中的每次運行測試命令時都將探索新的隨機數據. 如果你使用的是定期運行的自動化測試集成繫統, 隨機測試將特别有價值.</p>
|
|
|
|
|
<p><strong>練習 11.3:</strong> TestRandomPalindromes 隻測試了迴文字符串. 編寫新的隨機測試生成器, 用於測試隨機生成的非迴文字符串.</p>
|
|
|
|
|
<p><strong>練習 11.4:</strong> 脩改 randomPalindrome 函數, 以探索 IsPalindrome 對標點和空格的處理.</p>
|
|
|
|
|
@@ -2259,7 +2263,7 @@ ok gopl.io/ch11/word2 0.015s
|
|
|
|
|
<span class="hljs-keyword">return</span> <span class="hljs-constant">nil</span>
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全局變量的依賴. 我們還增加了一個全局名爲 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改爲不同的對象以便於檢査. 下面就是 echo_test.go 文件中的測試代碼:</p>
|
|
|
|
|
<p>在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸出是否正確, 我們通過增加參數來減少 echo 函數對全局變量的依賴. 我們還增加了一個全局名爲 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改爲不同的對象以便於檢査. 下面就是 echo_test.go 文件中的測試代碼:</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> main
|
|
|
|
|
|
|
|
|
|
<span class="hljs-keyword">import</span> (
|
|
|
|
|
@@ -2297,23 +2301,23 @@ ok gopl.io/ch11/word2 0.015s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導齣是被忽略的.</p>
|
|
|
|
|
<p>要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導出是被忽略的.</p>
|
|
|
|
|
<p>通過將測試放到表格中, 我們很容易添加新的測試用例. 讓我通過增加下面的測試用例來看看失敗的情況是怎麽樣的:</p>
|
|
|
|
|
<pre><code class="lang-Go">{<span class="hljs-constant">true</span>, <span class="hljs-string">","</span>, []<span class="hljs-typename">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}, <span class="hljs-string">"a b c\n"</span>}, <span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> wrong expectation!</span>
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p><code>go test</code> 輸齣如下:</p>
|
|
|
|
|
<p><code>go test</code> 輸出如下:</p>
|
|
|
|
|
<pre><code>$ go test gopl.io/ch11/echo
|
|
|
|
|
--- FAIL: TestEcho (0.00s)
|
|
|
|
|
echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n"
|
|
|
|
|
FAIL
|
|
|
|
|
FAIL gopl.io/ch11/echo 0.006s
|
|
|
|
|
</code></pre><p>錯誤信息描述了嚐試的操作(使用Go類似語法), 實際的行爲, 和期望的行爲. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.</p>
|
|
|
|
|
<p>要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.</p>
|
|
|
|
|
<p>要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退出; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.</p>
|
|
|
|
|
<h3 id="1123-白盒測試">11.2.3. 白盒測試</h3>
|
|
|
|
|
<p>一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行爲, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱爲 clear box 會更準確.)</p>
|
|
|
|
|
<p>黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.</p>
|
|
|
|
|
<p>我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導齣的, 因此它是白盒測試.</p>
|
|
|
|
|
<p>當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲.</p>
|
|
|
|
|
<p>我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導出的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導出的, 因此它是白盒測試.</p>
|
|
|
|
|
<p>當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸出對象, 因此測試代碼可以用另一個實現代替標準輸出, 這樣可以方便對比 echo 的輸出數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲.</p>
|
|
|
|
|
<p>下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯. 當用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.</p>
|
|
|
|
|
<pre><code class="lang-Go">gopl.io/ch11/storage1
|
|
|
|
|
|
|
|
|
|
@@ -2440,17 +2444,17 @@ FAIL gopl.io/ch11/echo 0.006s
|
|
|
|
|
<p>XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:</p>
|
|
|
|
|
<pre><code>$ go list -f={{.XTestGoFiles}} fmt
|
|
|
|
|
[fmt_test.go scan_test.go stringer_test.go]
|
|
|
|
|
</code></pre><p>有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因爲這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.</p>
|
|
|
|
|
</code></pre><p>有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導出一個內部的實現給測試擴展包. 因爲這些代碼隻有在測試時才需要, 因此一般放在 export_test.go 文件中.</p>
|
|
|
|
|
<p>例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是爲了避免太多的依賴, fmt 包併沒有導入包含鉅大表格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.</p>
|
|
|
|
|
<p>爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.</p>
|
|
|
|
|
<p>爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密出口導出了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密出口.</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> fmt
|
|
|
|
|
|
|
|
|
|
<span class="hljs-keyword">var</span> IsSpace = isSpace
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導齣了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試.</p>
|
|
|
|
|
<p>這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導出了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試.</p>
|
|
|
|
|
<h3 id="1125-編寫有效的測試">11.2.5. 編寫有效的測試</h3>
|
|
|
|
|
<p>許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.</p>
|
|
|
|
|
<p>Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律.</p>
|
|
|
|
|
<p>許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸出錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸出的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸出 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.</p>
|
|
|
|
|
<p>Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退出測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律.</p>
|
|
|
|
|
<p>下面的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 併停止程序. 它很方便使用也確實有效果, 但是當識别的時候, 錯誤時打印的信息幾乎是沒有價值的. 它併沒有爲解決問題提供一個很好的入口.</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">import</span> (
|
|
|
|
|
<span class="hljs-string">"fmt"</span>
|
|
|
|
|
@@ -2469,7 +2473,7 @@ FAIL gopl.io/ch11/echo 0.006s
|
|
|
|
|
<span class="hljs-comment">// ...</span>
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式齣現時引入抽象.</p>
|
|
|
|
|
<p>從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式出現時引入抽象.</p>
|
|
|
|
|
<pre><code class="lang-Go"><span class="hljs-keyword">func</span> TestSplit(t *testing.T) {
|
|
|
|
|
s, sep := <span class="hljs-string">"a:b:c"</span>, <span class="hljs-string">":"</span>
|
|
|
|
|
words := strings.Split(s, sep)
|
|
|
|
|
@@ -2481,12 +2485,12 @@ FAIL gopl.io/ch11/echo 0.006s
|
|
|
|
|
}
|
|
|
|
|
</code></pre>
|
|
|
|
|
<p>現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 併且打印的眞實返迴的值和期望返迴的值; 併且卽使斷言失敗依然會繼續嚐試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的表驅動測試那樣來準備更多的 s, sep 測試用例.</p>
|
|
|
|
|
<p>前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.</p>
|
|
|
|
|
<p><strong>練習11.5:</strong> 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸齣結果.</p>
|
|
|
|
|
<p>前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後才是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.</p>
|
|
|
|
|
<p><strong>練習11.5:</strong> 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸出結果.</p>
|
|
|
|
|
<h3 id="1126-避免的不穩定的測試">11.2.6. 避免的不穩定的測試</h3>
|
|
|
|
|
<p>如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處.</p>
|
|
|
|
|
<p>當一個測試函數産生一個複雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸齣部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.</p>
|
|
|
|
|
<p>避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試.</p>
|
|
|
|
|
<p>如果一個應用程序對於新出現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處.</p>
|
|
|
|
|
<p>當一個測試函數産生一個複雜的輸出如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸出的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸出部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.</p>
|
|
|
|
|
<p>避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸出中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
@@ -2518,7 +2522,7 @@ FAIL gopl.io/ch11/echo 0.006s
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
require(["gitbook"], function(gitbook) {
|
|
|
|
|
var config = {"highlight":{},"sharing":{"facebook":true,"twitter":true,"google":false,"weibo":false,"instapaper":false,"vk":false,"all":["facebook","google","twitter","weibo","instapaper"]},"fontsettings":{"theme":"white","family":"sans","size":2}};
|
|
|
|
|
var config = {"katex":{},"highlight":{},"sharing":{"facebook":true,"twitter":true,"google":false,"weibo":false,"instapaper":false,"vk":false,"all":["facebook","google","twitter","weibo","instapaper"]},"fontsettings":{"theme":"white","family":"sans","size":2}};
|
|
|
|
|
gitbook.start(config);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|