【问题标题】:Check if function will call another function or not检查函数是否会调用另一个函数
【发布时间】: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


    【解决方案1】:

    简而言之,你没有。如果需要验证,您可以在它自己的单元测试中测试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)
    }
    

    现在,helloworld 函数很容易测试。以你好为例:

    // 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

    【讨论】:

    • 谢谢。我读了第二部分,两次。但还是看不懂。
    • 世界上有完整的单元测试项目吗?
    • @AminShojaei 有些肯定是。你到底有什么不明白的?
    • TestGreet() 令人困惑。这背后的想法是什么?
    • @AminShojaei 用于测试该函数的行为——因为它没有返回值。因此,您添加了一个可以读取的缓冲区,然后您可以对写入该缓冲区的结果进行世界上所有的测试。
    【解决方案2】:

    您可以使用“测试挂钩”。

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

    https://play.golang.org/p/zd3BpliFgnu

    【讨论】:

      【解决方案3】:

      你可以做这些,但通常第二个(接口抽象)是要走的路:

      1. 将您的函数分配给一个包私有变量,然后在测试中像这样创建一个简单的包装器(我们必须将函数重新分配给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")
          }
      }
      
      1. 如果您的函数是一个方法(属于一个结构),或者包含方法,您应该创建一个接口,允许您创建一个简单的模拟,进而允许我们记录调用的计数器。
      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")
          }
      }
      

      请注意,我们的模拟方法必须具有对结构的指针引用,否则我们将传递结构的副本并且无法增加计数器。

      【讨论】:

      • 谢谢。第一种方法不是有点混乱吗?把所有的函数都放到变量里面可以吗?
      • 这很混乱,因为测试用例有点混乱 :) 大多数时候你不测试私人合约,如果 bang 函数在外部做了一些事情(比如 db 调用),你应该尝试接受它作为big 函数的参数。为了使其更简洁,您还可以定义一个函数类型,例如 type banger func(t int) 并将 big 签名更改为 big(t int, bang banger)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-15
      • 2021-12-15
      • 2017-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多