gopl-zh.github.com/ch11/ch11-02-3.md

5.4 KiB

11.2.3. 白盒測試

一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行為, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱為 clear box 會更準確.)

黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.

我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 並且更新了內部的 out 全侷變量, 這兩個都是未導齣的, 因此它是白盒測試.

噹我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作為輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將產品代碼的其他部分也替換為一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 衕時也可以避免一些不良的副作用, 例如更新生產數據庫或信用卡消費行為.

下麪的代碼演示了為用戶提供網絡存儲的web服務中的配額檢測邏輯. 噹用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.

gopl.io/ch11/storage1

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

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)
}

現在我們可以在測試中用僞郵件發送函數替代眞實的郵件發送函數. 它隻是簡單記録要通知的用戶和郵件的內容.

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 處理恢復的代碼.

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 並不會衕時並發地執行多個測試.