【发布时间】:2020-08-16 09:54:02
【问题描述】:
我愿意为我的 Go 应用程序编写单元测试。
有一个函数会调用另一个函数,我应该如何确认这个调用?
// the function which I wanna test
func big(t int) {
bang(t * 6) // how to confirm this?
}
我无法模拟 bang(),因为它不属于任何结构。
【问题讨论】:
标签: unit-testing go
我愿意为我的 Go 应用程序编写单元测试。
有一个函数会调用另一个函数,我应该如何确认这个调用?
// the function which I wanna test
func big(t int) {
bang(t * 6) // how to confirm this?
}
我无法模拟 bang(),因为它不属于任何结构。
【问题讨论】:
标签: unit-testing go
简而言之,你没有。如果需要验证,您可以在它自己的单元测试中测试bang。然而,拥有没有返回值的函数是很棘手的——你只能在它们与某些东西交互时测试它们的行为。
TL;DR:如果您需要测试行为,请尽可能简单地实际测试行为。尤其是当您测试行为时,请彻底测试:这会使您的代码更加健壮。
注意:有多种方法可以测试行为。我正在展示一个简单的例子。
一个“更好”(阅读:更具体和透明)的示例如下所示:
func hello(lang string) string {
switch lang {
case "de":
return "Hallo"
case "es":
return "Hola"
default:
return "Hello"
}
}
func world(lang string) string {
switch lang {
case "de":
return "Welt"
case "es":
return "mundo"
default:
return "world"
}
}
func greet(lang string) {
var g string
switch lang {
case "de":
g = fmt.Sprintf("%s, %s!", hello("de"), world("de"))
case "es":
g = fmt.Sprintf("¡%s, %s!", hello("es"), world("es"))
default:
g = fmt.Sprintf("%s, %s!", hello(lang), world(lang))
}
fmt.Println(g)
}
现在,hello 和 world 函数很容易测试。以你好为例:
// TestHello is heavily simplified for brevity.
func TestHello(t *testing.T) {
testCases := []struct {
desc string
lang string
expected string
}{
{
desc: "German",
lang: "de",
expected: "Hallo",
},
{
desc: "Spanish",
lang: "es",
expected: "Hola",
},
{
desc: "Default",
lang: "en",
expected: "Hello",
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
if hello(tC.lang) != tC.expected {
t.Fail()
}
})
}
}
对world 重复此操作,您就会知道这两个函数的行为完全符合您的预期。
但是你将如何测试greet?它没有返回值,在当前形式下,确保写入内容的唯一方法是实际重定向os.Stdout 的输出。这可能就足够了,具体取决于您的用例,但它既麻烦又冗长。
因此,增强代码的可测试性更有意义:
// dsts is variadic to make it optional.
// So you can either call it as greet("en")
// or greet("en",whatever), with the added bonus
// that you can write to multiple destinations.
func greet(lang string, dsts ...io.Writer) {
var g string
var out io.Writer
if dsts == nil {
// If nothing is set, we want the default behavior.
out = os.Stdout
} else if len(dsts) == 1 {
// If only one dst is set for example for unit tests *wink* *wink*
// we can write to it directly.
out = dsts[0]
} else {
// In case multiple dsts are set, we utilize multiwriter
out = io.MultiWriter(dsts...)
}
switch lang {
case "de":
g = fmt.Sprintf("%s, %s!", hello("de"), world("de"))
case "es":
g = fmt.Sprintf("¡%s, %s!", hello("es"), world("es"))
default:
g = fmt.Sprintf("%s, %s!", hello(lang), world(lang))
}
fmt.Fprintln(out, g)
}
测试这个变得非常简单:
func TestGreet(t *testing.T) {
buf := bytes.NewBuffer(nil)
for _, lang := range []string{"de", "es", "unknown"} {
greet(lang, buf)
greeting := buf.String()
// Of course, testing should be a bit more thorough
if greeting == "" {
t.Errorf("greeting for %s is empty!", lang)
}
if !strings.HasSuffix(greeting, "!\n") {
t.Errorf("greeting '%s' for language '%s' has no exclamation mark or newline", greeting, lang)
}
}
}
更高级的技术(例如用于数据库连接)将发送至use mocks。
【讨论】:
TestGreet() 令人困惑。这背后的想法是什么?
您可以使用“测试挂钩”。
var testHookBang func(int)
func big(t int) {
bang(t * 6) // how to confirm this?
}
func bang(i int) {
if testHookBang != nil {
testHookBang(i)
}
// ...
}
然后在你的测试中你可以这样做:
func TestBig(t *testing.T) {
defer func() { testHookBang = nil }()
var bangFunc struct {
invoked bool
argument int
}
testHookBang = func(i int) {
bangFunc.invoked = true
bangFunc.argument = i
}
tests := []struct {
bigArg int
wantBangArg int
}{
{10, 60},
{5, 30},
}
for _, tt := range tests {
bangFunc.invoked = false
bangFunc.argument = 0
big(tt.bigArg)
if !bangFunc.invoked {
t.Error("big did not invoke bang")
}
if bangFunc.argument != tt.wantBangArg {
t.Errorf("unexpected bang argument: got %d want %d", bangFunc.argument, tt.wantBangArg)
}
}
}
【讨论】:
你可以做这些,但通常第二个(接口抽象)是要走的路:
originalBang,以避免无限回调循环):var bang = func(t int) {
// your actual implementation
}
func big(t int) {
bang(t * 6)
}
func TestBig(t *testing.T) {
called := 0
originalBang := bang
testBang := func(t int) {
called += 1
originalBang(t)
}
bang = testBang
big(5)
if called != 1 {
t.Error("bang was not called")
}
}
type Bang struct {
// your actual implementation
}
func (b Bang) bang(t int) {
// your actual implementation
}
type Banger interface {
bang(int)
}
type mockBanger struct {
called int
}
func (m *mockBanger) bang(t int) {
m.called++
}
func big(t int, banger Banger) {
banger.bang(t)
}
func TestBig(t *testing.T) {
mock := &mockBanger{}
big(5, mock)
if mock.called != 1 {
t.Error("bang was not called")
}
}
请注意,我们的模拟方法必须具有对结构的指针引用,否则我们将传递结构的副本并且无法增加计数器。
【讨论】:
big 函数的参数。为了使其更简洁,您还可以定义一个函数类型,例如 type banger func(t int) 并将 big 签名更改为 big(t int, bang banger)。