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

135 lines
5.4 KiB
Markdown
Raw Normal View History

2015-12-09 07:45:11 +00:00
### 11.2.3. 白盒測試
2015-12-15 02:08:24 +00:00
一个测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理. 黑盒测试只需要测试包公开的文档和API行为, 内部实现对测试代码是透明的. 相反, 白盒测试有访问包内部函数和数据结构的权限, 因此可以做到一下普通客户端无法实现的测试. 例如, 一个饱和测试可以在每个操作之后检测不变量的数据类型. (白盒测试只是一个传统的名称, 其实称为 clear box 会更准确.)
黑盒和白盒这两种测试方法是互补的. 黑盒测试一般更健壮, 随着软件实现的完善测试代码很少需要更新. 它们可以帮助测试者了解真是客户的需求, 可以帮助发现API设计的一些不足之处. 相反, 白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖.
我们已经看到两种测试的例子. TestIsPalindrome 测试仅仅使用导出的 IsPalindrome 函数, 因此它是一个黑盒测试. TestEcho 测试则调用了内部的 echo 函数, 并且更新了内部的 out 全局变量, 这两个都是未导出的, 因此它是白盒测试.
当我们开发TestEcho测试的时候, 我们修改了 echo 函数使用包级的 out 作为输出对象, 因此测试代码可以用另一个实现代替标准输出, 这样可以方便对比 echo 的输出数据. 使用类似的技术, 我们可以将产品代码的其他部分也替换为一个容易测试的伪对象. 使用伪对象的好处是我们可以方便配置, 容易预测, 更可靠, 也更容易观察. 同时也可以避免一些不良的副作用, 例如更新生产数据库或信用卡消费行为.
下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑. 当用户使用了超过 90% 的存储配额之后将发送提醒邮件.
```Go
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 函数.
```Go
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)
}
```
现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数. 它只是简单记录要通知的用户和邮件的内容.
```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 并不会同时并发地执行多个测试.