风格指南中关于测试的大部分建议都是关于测试你自己的代码。本节是关于如何为其他人提供设施来测试他们编写的代码,以确保它符合你的库的要求。
这种测试被称为验收测试。这种测试的前提是,使用测试的人不知道测试中发生的每一个细节;他们只是把输入交给测试机构来完成。这可以被认为是一种控制反转的形式。
在典型的Go测试中,测试函数控制着程序流程,无断言和测试函数指南鼓励你保持这种方式。本节解释了如何以符合 Go 风格的方式来编写对这些测试的支持。
在深入探讨如何做之前,请看下面摘录的io/fs
中的一个例子:
type FS interface {
Open(name string) (File, error)
}
虽然存在众所周知的fs.FS
的实现,但 Go 开发者可能会被期望编写一个。为了帮助验证用户实现的fs.FS
是否正确,在testing/fstest
中提供了一个通用库,名为fstest.TestFS
。这个API将实现作为一个黑箱来处理,以确保它维护了io/fs
契约的最基本部分。
现在我们知道了什么是验收测试以及为什么要使用验收测试,让我们来探讨为package chess
建立一个验收测试,这是一个用于模拟国际象棋游戏的包。chess
的用户应该实现 chess.Player
接口。这些实现是我们要验证的主要内容。我们的验收测试关注的是棋手的实现是否走了合法的棋,而不是这些棋是否聪明。
1.为验证行为创建一个新的包,习惯上命名为,在包名后面加上 “test “一词(例如:chesstest
)。
2.创建执行验证的函数,接受被测试的实现作为参数,并对其进行练习:
// ExercisePlayer tests a Player implementation in a single turn on a board.
// The board itself is spot checked for sensibility and correctness.
//
// It returns a nil error if the player makes a correct move in the context
// of the provided board. Otherwise ExercisePlayer returns one of this
// package's errors to indicate how and why the player failed the
// validation.
func ExercisePlayer(b *chess.Board, p chess.Player) error
测试应该注意哪些不变式被破坏,以及如何破坏。你的设计可以选择两种失败报告的原则:
快速失败:一旦实现违反了一个不变式,就返回一个错误。
这是最简单的方法,如果预计验收测试会快速执行,那么它的效果很好。简单的错误 sentinels和自定义类型在这里可以很容易地使用,反过来说,这使得测试验收测试变得很容易。
for color, army := range b.Armies {
// The king should never leave the board, because the game ends at
// checkmate.
if army.King == nil {
return &MissingPieceError{Color: color, Piece: chess.King}
}
}
集合所有的失败:收集所有的失败,并报告它们。 这种方法类似于keep going的指导,如果验收测试预计会执行得很慢,这种方法可能更可取。
你如何聚集故障,应该由你是否想让用户或你自己有能力询问个别故障(例如,为你测试你的验收测试)来决定的。下面演示了使用一个自定义错误类型,聚合错误。
var badMoves []error
move := p.Move()
if putsOwnKingIntoCheck(b, move) {
badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move})
}
if len(badMoves) > 0 {
return SimulationError{BadMoves: badMoves}
}
return nil
验收测试应该遵守 keep going 的指导,不调用t.Fatal
,除非测试检测到被测试系统中的不变量损坏。 例如,t.Fatal
应该保留给特殊情况,如设置失败,像往常一样:
func ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error {
t.Helper()
if cfg.Simulation == Modem {
conn, err := modempool.Allocate()
if err != nil {
t.Fatalf("no modem for the opponent could be provisioned: %v", err)
}
t.Cleanup(func() { modempool.Return(conn) })
}
// Run acceptance test (a whole game).
}
这种技术可以帮助你创建简明、规范的验证。但不要试图用它来绕过断言指南。 最终产品应该以类似这样的形式提供给终端用户:
// Good:
package deepblue_test
import (
"chesstest"
"deepblue"
)
func TestAcceptance(t *testing.T) {
player := deepblue.New()
err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player)
if err != nil {
t.Errorf("deepblue player failed acceptance test: %v", err)
}
}