3月10日《Using Go Flags in Tests》
flag 包在go test 中的作用
原文链接:https://blog.jbowen.dev/2019/08/using-go-flags-in-tests/
Go 标准库中的 flag 包为 Go 程序提供了添加命令行选项和参数解析的简单方法。
Go Test 命令
go test命令的工作原理:当运行go test时,它会将以_test.go结尾的所有文件编译为测试二进制文件,然后运行该二进制文件来执行测试。
由于 Go test 的也是一个编译后的可执行二进制文件的 Go 程序,它可以像任何其他程序一样处理命令行参数。
Parsing Flags
在程序中使用 flag 包的标准方法是定义一个或多个 flag 变量,并在程序的 main() 函数中调用 flag.Parse() 命令。
示例:
func Greet(loc string)string {
return fmt.Sprintf("Hello, %s!\n", loc)
}
func main() {
var loc string
flag.StringVar(&loc, "location", "World", "Name of location to greet")
flag.Parse()
fmt.Println(Greet(loc))
}
接下来,运行这个程序:
$ go run hello.go
Hello, World!
$ go run hello.go -location 'San Francisco'
Hello, San Francisco!
可以看到在 main 函数中,需要调用 flag.Parse() 方法才能获取 命令后传入的参数。 \n
Parsing Test Flags
上述方法不适用于 Go 测试,因为我们通常不会为单元测试编写 main() 函数,我们只是编写测试函数并让工具从那里处理事情。 那么我们从哪里调用 flag.Parse 呢?
事实证明,go test 生成的测试二进制文件已经在内部使用了 flag 包,并且在正常运行期间调用了 flag.Parse()。 我们需要做的就是将我们的标志变量定义为全局变量,以便在运行 flag.Parse() 之前知道它们。
最简单的方法是定义我们的全局变量设置为来自 flag.String、flag.Boolean 等的返回值。 例如:
var loc = flag.String("location", "World", "Name of location to greet")
func TestGreet(t *testing.T) {
res := Greet(*loc)
if res != "Hello, San Francisco!" {
t.Errorf("String mismatch on test")
}
}
值得注意的是,loc 的类型是 *string ,而不是 string 。
现在,当我们运行测试时,我们可以看到,只有在“San Francisco”作为参数通过时,测试才会通过:
$ go test
--- FAIL: TestGreet (0.00s)
hello_test.go:20: String mismatch on test
FAIL
exit status 1
FAIL _/home/jbowen 0.001s
$ go test -location 'Los Angeles'
--- FAIL: TestGreet (0.00s)
hello_test.go:20: String mismatch on test
FAIL
exit status 1
FAIL _/home/jbowen 0.001s
$ go test -location 'San Francisco'
PASS
ok _/home/jbowen 0.001s
OK .. But Why?
使用命令行标志来确定测试是否通过并不是一个很好的用例,但值得考虑还有其他标志的用例使这种方法:
有选择的去运行或者不允许某些 test
我使用测试标志的一个用例是,只有在设置了布尔标志时才能运行测试(或多项测试)。在编写代码访问基于需要访问网络的API时,这非常有用。通过offline定义标志,我可以在任何需要网络访问的测试之前进行简单的测试:
var offline = flag.Bool("offline", false, "only perform local tests")
func TestSomethingRemote(t *testing.T) {
if *offline {
t.Skip("skipping test in offline mode")
}
...
}
使用上述代码,只有在*offline为false时,才会运行TestSomethingRemote()。通过-offlinego test,我们可以自动跳过此测试......非常适合在您可能无法在线访问时(如飞行时)进行快速测试。
发送配置
一些测试(在处理远程资源时经常发生)可能需要不应共享或存储在代码存储库中的值(例如API密钥、密码、用户名)。在不将其保存到 git 存储库的情况下将这些参数提供给单元测试的一种可能方法是将它们作为标志参数传递(或者,如果有多个参数,则以要解析值的配置文件的名义传递)。
生成 ’Golden‘ 结果
Mitchell Hashimoto(HashiCorp 的创始人和 Vagrant 等工具的创建者)在他的演示文稿 Advanced Testing With Go 中概述了使用测试标志来保存“黄金”结果的用例。
黄金结果是一种在某些代码中轻松测试更复杂输出或状态的方法,方法是将已知良好单元测试的输出保存到文件中,然后使用此输出与未来的测试进行比较。
当格式或内容将来可能发生变化时,这尤其有用。 Mitchell 的示例展示了如何使用 -update 标志来触发 go test 程序生成新的黄金文件而不是执行测试。 这样,相同的代码用于生成和比较黄金文件。
var update = flag.Bool("update",false,"update golden files")
func TestAdd(t *testing.T){
// ... table(probably!)
for _, tc := range cases{
actual := doSomething(tc)
golden := filepath.join("test-fixtures", tc.Name + ".golden")
if *update {
iotuil.WriteFile(golden, actual, 0644)
}
expected, _ := iotuil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
// FAIL
}
}
}
注意事项
go test 的内置行为有一些需要注意的问题:
与flag 中的内置名称重复
go test 命令有许多标志选项,它会对其进行解析以供自己使用。 如果您使用与内置选项相同的名称定义标志,则测试二进制文件本身不会看到它,因为 go 工具将在执行生成的测试二进制文件之前使用它。
例如,如果我们定义一个名为short布尔标志,并尝试在我们的程序中访问,它将不起作用,因为-short是go test的标志。
package main
import (
"flag"
"testing"
)
var short = flag.Bool("short", false, "short")
func TestShort(t *testing.T) {
if !*short {
t.Fail()
}
}
如果我们使用-short选项调用go test,我们的测试函数仍然将short变量视为false,因为测试二进制文件的 flag.Parse() 从未看到该选项。
$ go test -short
--- FAIL: TestShort (0.00s)
FAIL
我们可以通过编译(-c)并将测试二进制文件保存为名为“test”(-o test)的文件,然后直接调用它来证明这是由于go test:
$ go test -c -o test
$ ./test
--- FAIL: TestShort (0.00s)
FAIL
$ ./test -short
PASS
解决这个问题的一个变通办法是使用-args选项go test,以指示所有剩余参数都打算直接传递给测试二进制文件:
$ go test -args -short
PASS
不过,最好重命名您的参数,以免与现有标志冲突。