### 11.2.3. 白盒測試
一種測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理。黑盒測試隻需要測試包公開的文檔和API行爲,內部實現對測試代碼是透明的。相反,白盒測試有訪問包內部函數和數據結構的權限,因此可以做到一下普通客戶端無法實現的測試。例如,一個白盒測試可以在每個操作之後檢測不變量的數據類型。(白盒測試隻是一個傳統的名稱,其實稱爲clear box測試會更準確。)
黑盒和白盒這兩種測試方法是互補的。黑盒測試一般更健壯,隨着軟件實現的完善測試代碼很少需要更新。它們可以幫助測試者了解眞是客戶的需求,也可以幫助發現API設計的一些不足之處。相反,白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋。
我們已經看到兩種測試的例子。TestIsPalindrome測試僅僅使用導出的IsPalindrome函數,因此這是一個黑盒測試。TestEcho測試則調用了內部的echo函數,併且更新了內部的out包級變量,這兩個都是未導出的,因此這是白盒測試。
當我們準備TestEcho測試的時候,我們脩改了echo函數使用包級的out變量作爲輸出對象,因此測試代碼可以用另一個實現代替標準輸出,這樣可以方便對比echo輸出的數據。使用類似的技術,我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象。使用僞對象的好處是我們可以方便配置,容易預測,更可靠,也更容易觀察。同時也可以避免一些不良的副作用,例如更新生産數據庫或信用卡消費行爲。
下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯。當用戶使用了超過90%的存儲配額之後將發送提醒郵件。
gopl.io/ch11/storage1
```Go
package storage
import (
"fmt"
"log"
"net/smtp"
)
func bytesInUse(username string) int64 { return 0 /* ... */ }
// Email sender configuration.
// NOTE: never put passwords in source code!
const sender = "notifications@example.com"
const password = "correcthorsebatterystaple"
const hostname = "smtp.example.com"
const template = `Warning: you are using %d bytes of storage,
%d%% of your quota.`
func CheckQuota(username string) {
used := bytesInUse(username)
const quota = 1000000000 // 1GB
percent := 100 * used / quota
if percent < 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
auth := smtp.PlainAuth("", sender, password, hostname)
err := smtp.SendMail(hostname+":587", auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf("smtp.SendMail(%s) failed: %s", username, err)
}
}
```
我們想測試這個代碼,但是我們併不希望發送眞實的郵件。因此我們將郵件處理邏輯放到一個私有的notifyUser函數中。
gopl.io/ch11/storage2
```Go
var notifyUser = func(username, msg string) {
auth := smtp.PlainAuth("", sender, password, hostname)
err := smtp.SendMail(hostname+":587", auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
}
}
func CheckQuota(username string) {
used := bytesInUse(username)
const quota = 1000000000 // 1GB
percent := 100 * used / quota
if percent < 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
notifyUser(username, msg)
}
```
現在我們可以在測試中用僞郵件發送函數替代眞實的郵件發送函數。它隻是簡單記録要通知的用戶和郵件的內容。
```Go
package storage
import (
"strings"
"testing"
)
func TestCheckQuotaNotifiesUser(t *testing.T) {
var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) {
notifiedUser, notifiedMsg = user, msg
}
// ...simulate a 980MB-used condition...
const user = "joe@example.org"
CheckQuota(user)
if notifiedUser == "" && notifiedMsg == "" {
t.Fatalf("notifyUser not called")
}
if notifiedUser != user {
t.Errorf("wrong user (%s) notified, want %s",
notifiedUser, user)
}
const wantSubstring = "98% of your quota"
if !strings.Contains(notifiedMsg, wantSubstring) {
t.Errorf("unexpected notification message <<%s>>, "+
"want substring %q", notifiedMsg, wantSubstring)
}
}
```
這里有一個問題:當測試函數返迴後,CheckQuota將不能正常工作,因爲notifyUsers依然使用的是測試函數的僞發送郵件函數(當更新全局對象的時候總會有這種風險)。 我們必須脩改測試代碼恢複notifyUsers原先的狀態以便後續其他的測試沒有影響,要確保所有的執行路徑後都能恢複,包括測試失敗或panic異常的情形。在這種情況下,我們建議使用defer語句來延後執行處理恢複的代碼。
```Go
func TestCheckQuotaNotifiesUser(t *testing.T) {
// Save and restore original notifyUser.
saved := notifyUser
defer func() { notifyUser = saved }()
// Install the test's fake notifyUser.
var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) {
notifiedUser, notifiedMsg = user, msg
}
// ...rest of test...
}
```
這種處理模式可以用來暫時保存和恢複所有的全局變量,包括命令行標誌參數、調試選項和優化參數;安裝和移除導致生産代碼産生一些調試信息的鉤子函數;還有有些誘導生産代碼進入某些重要狀態的改變,比如超時、錯誤,甚至是一些刻意製造的併發行爲等因素。
以這種方式使用全局變量是安全的,因爲go test命令併不會同時併發地執行多個測試。