Golang测试模拟函数的最佳实践

huangapple go评论90阅读模式
英文:

Golang test mock functions best practices

问题

我正在为我的代码开发一些测试(使用testing包),我想知道在被测试的函数内部模拟函数的最佳方法是什么:

我应该将函数作为参数传递吗?
在这种情况下,如果该函数调用另一个函数怎么办?在被测试的函数中,我应该将第一个和第二个函数都作为参数传递吗?

注意:其中一些函数是在对象上调用的(例如someObj.Create()),并使用HTTP API调用。

更新以进行澄清:

示例:函数

func f1() error {
  ... //一些API调用
}

func (s *SomeStruct) f2() error {
  return f1
}

func f3() error {
  return nil
}

func f4() error {
  ...
  err = obj.f2()
  ...
  err = f3()
  ...
}

对于上述代码:如果我想测试f4,最佳的模拟f2和f3的方法是什么?

如果我将f2和f3作为参数传递给f4,那么它将起作用,但是那么f2的测试怎么办?我应该将f1作为参数传递给f2吗?

如果是这样,那么f4的参数中是否也应该有f1?

英文:

I am developing some tests for my code (using the testing package), and I am wondering what's the best way to mock functions inside the tested function:

Should I pass the function as parameter?
In that case, what if that function calls another function? Should I pass both the first and second function as parameters in the tested one?

Note: some of the functions are called on objects (i.e. someObj.Create()) and use HTTP API calls.

UPDATE for clarification:

Example: functions

func f1() error {
  ... //some API call
}

func (s *SomeStruct) f2() error {
  return f1
}

func f3() error {
  return nil
}

func f4() error {
  ...
  err = obj.f2()
  ...
  err = f3()
  ...
}

For the above: if I want to test f4, what's the best way to mock f2 and f3?

If I pass f2 and f3 to f4 as parameters it would work, but then what for the f2 test? Should I pass f1 to f2 as parameter?

And if that's it, should then f4 have f1 as well in the parameters?

答案1

得分: 7

作为一般准则,函数不太容易进行模拟,因此我们最好模拟实现某个特定接口的结构体,这些结构体可能会被传入函数以测试代码的不同分支。以下是一个基本示例。

package a

import "fmt"

type DoSomethingInterface interface {
	DoSomething() error
}

func DoSomething(a DoSomethingInterface) {
	if err := a.DoSomething(); err != nil {
		fmt.Println("error occurred")
		return
	}
	fmt.Println("no error occurred")
	return
}
package a_test

import (
	"errors"
	"testing"

	"path/to/a"
)

type simpleMock struct {
	err error
}

func (m *simpleMock) DoSomething() error {
	return m.err
}

func TestDoSomething(t *testing.T) {
	errorMock := &simpleMock{err: errors.New("some error")}
	a.DoSomething(errorMock)
	// 测试是否记录了 "error occurred"

	regularMock := &simpleMock{}
	a.DoSomething(regularMock)
	// 测试是否记录了 "no error occurred"
}

在上面的示例中,你可以测试 DoSomething 函数及其发生的分支。例如,你可以创建一个带有错误的模拟实例来测试一个测试用例,然后创建另一个没有错误的模拟实例来测试另一个情况。相应的情况是测试某个特定字符串是否已记录到标准输出;在这种情况下,当使用错误实例化 simpleMock 时,它将是 "error occurred",而当没有错误实例化 simpleMock 时,它将是 "no error occurred"。

当然,这可以扩展到其他情况,例如 DoSomething 函数实际上返回某种值,你想对该值进行 assertion

编辑:

我更新了代码,考虑到接口位于另一个包中的情况。请注意,新更新的代码中有一个包 a,其中包含接口和待测试的函数,还有一个包 a_test,仅仅是一个关于如何测试 a.DoSomething 的模板。

英文:

As a general guideline, functions aren't very mockable so its in our best interests to mock structs that implement a certain interface that may be passed into functions to test the different branches of code. See below for a basic example.

package a

type DoSomethingInterface interface {
	DoSomething() error
}


func DoSomething(a DoSomethingInterface) {
	if err := a.DoSomething(); err != nil {
		fmt.Println("error occurred")
		return
	}
	fmt.Println("no error occurred")
    return
}

package a_test

import (
	"testing"
	"<path to a>/a"
)

type simpleMock struct {
	err error
}

func (m *simpleMock) DoSomething() error {
	return m.err
}

func TestDoSomething(t *testing.T) {
	errorMock := &simpleMock{errors.New("some error")}
	a.DoSomething(errorMock)
	// test that "an error occurred" is logged

	regularMock := &simpleMock{}
	a.DoSomething(regularMock)
	// test "no error occurred" is logged
}

In the above example, you would test the DoSomething function and the branches that happens eg. you would create an instance of the mock with an error for one test case and create another instance of the mock without the error to test the other case. The respective cases are to test a certain string has been logged to standard out; in this case it would be "error occurred" when simpleMock is instantiated with an error and "no error occurred" when there simpleMock is not instantiated with an error.

This can of course be expanded to other cases eg. the DoSomething function actually returns some kind of value and you want to make an assertion on the value.

Edit:

I updated the code with the concern that the interface lives in another package. Note that the new updated code has a package a that contains the interface and the function under test and a package a_test that is merely a template of how to approach testing a.DoSomething.

答案2

得分: 0

我不确定你在这里想要做什么,但我会解释一下如何在Go中进行测试。

假设我们有一个具有以下目录结构的应用程序:

root/
  pack1/
    pack1.go
    pack1_test.go
  pack2/
    pack2.go
    pack2_test.go
  main.go
  main_test.go

我们假设pack2.go中有你想要测试的函数:

package pack2 

func f1() error {
  ... //一些API调用
}

func (s *SomeStruct) f2() error {
  return f1
}

func f3() error {
  return nil
}

func f4() error {
  ...
  err = obj.f2()
  ...
  err = f3()
  ...
}

到目前为止看起来不错。现在,如果你想要测试pack2中的函数,你可以创建一个名为pack2_test.go的文件。在Go中,所有的测试文件都以类似的方式命名(packagename_test.go)。现在让我们看一下一个典型的包测试的内部情况(以此示例中的pack2_test.go为例):

package pack2

import (
  "testing"
  "fmt"
)

TestF1(*testing.T) {
  x := "something for testing"
  f1() // 这个测试来自于“pact2.go”包中的f1函数
}


TestF2(*testing.T) {
    y := new(somestruct) 
    y.f2() // 测试来自于“pact2.go”包中的f2函数
}


TestF3(*testing.T) {
   /// 一些代码
   f3() // 测试f3函数
}


TestF4(*testing.T) {
    /// 代码
    f3() // 你明白了吧
}

让我解释一下。注意在pack2_test.go中,第一行指定了包名为pack2。简而言之,这意味着我们在pack2的“作用域”中,因此可以像在pack2内部一样调用pack2中的所有函数。这就是为什么在Testf*函数中,我们可以调用pack2中的函数。另一个要注意的是导入的包"testing"。它有两个作用:

首先,它提供了一些用于运行测试的功能。我不会详细介绍。
其次,它帮助确定go test应该运行哪些函数。

现在来看这些函数。在测试包中,任何具有前缀“Test”和参数“t *testing.T”的函数(当你不需要使用测试功能时,可以使用“*testing.T”)在运行go test时都会被执行。你可以使用变量t来引用我提到的测试功能。你也可以在带有前缀的函数内部声明函数并调用它们。

所以,如果我在终端中运行go test,它将执行你想要测试的函数,这些函数在pack2_test.go中指定。

你可以在这里和这里了解更多关于测试的信息。

英文:

I'm not sure what you're trying to do here but I'll explain how testing should be done in Go.

Lets say we have an application with the following directory hierarchy:

root/
  pack1/
    pack1.go
    pack1_test.go
  pack2/
    pack2.go
    pack2_test.go
  main.go
  main_test.go

We'll assume that pack2.go has the functions you want to test:

package pack2 

func f1() error {
  ... //some API call
}

func (s *SomeStruct) f2() error {
  return f1
}

func f3() error {
  return nil
}

func f4() error {
  ...
  err = obj.f2()
  ...
  err = f3()
  ...
}

Looks good so far. Now if you want to test the functions in pack2, you would create a file called pack2_test.go. All test files in go are named similarly (packagename_test.go). Now lets see the inside of a typical test for a package (pack2_test.go in this example):

package pack2

import (
  "testing"
  "fmt"
)

TestF1(*testing.T) {
  x := "something for testing"
  f1() // This tests f1 from the package "pact2.go"
}


TestF2(*testing.T) {
    y := new(somestruct) 
    y.f2() // tests f2 from package "pact2.go"
}


TestF3(*testing.T) {
   /// some code
   f3() // tests f3
}


TestF4(*testing.T) {
    /// code
    f3() // you get the gist
}

Let me explain. Notice how in pack2_test.go, the first line says that the package is pack2. In a nutshell, this means that we're in the "scope" of the package pack2 and thus all the functions found in pack2 can be called as if you're within pack2. Thats why, within the Testf* functions, we could've called the functions from pack2. Another thing to note is the imported package "testing". This helps with two things:

First, it provides some functionality for running tests. I won't go into that.
Second, it helps identify the functions that go test should run.

Now to the functions. Any function within a test package that has the prefix "Test" and the parameters "t *testing.T" (you can use "*testing.T" when you don't need to use the testing functionality) will be executed when you run go test. You use the variable t to reference the testing functionality I mentioned. You can also declare functions without the prefix and call them within the prefixed functions.

So, if I go to my terminal and run go test, it will execute the functions you want to test, specified in pack2_test.go

You can learn more about testing here and here

huangapple
  • 本文由 发表于 2016年4月18日 16:26:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/36688872.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定