简体   繁体   English

Go 中的模拟函数

[英]Mock functions in Go

I'm puzzled with dependencies.我对依赖性感到困惑。 I want to be able to replace some function calls with mock ones.我希望能够用模拟电话替换一些 function 电话。 Here's a snippet of my code:这是我的代码片段:

func get_page(url string) string {
    get_dl_slot(url)
    defer free_dl_slot(url)
    
    resp, err := http.Get(url)
    if err != nil { return "" }
    defer resp.Body.Close()
    
    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil { return "" }
    return string(contents)
}

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := get_page(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

I'd like to be able to test downloader() without actually getting a page through http - ie by mocking either get_page (easier since it returns just the page content as a string) or http.Get() .我希望能够测试downloader()而无需通过 http 实际获取页面 - 即通过 mocking get_page (更简单,因为它仅将页面内容作为字符串返回)或http.Get()

I foundthis thread which seems to be about a similar problem.我发现这个线程似乎是关于类似的问题。 Julian Phillips presents his library, Withmock as a solution, but I'm unable to get it to work. Julian Phillips 展示了他的库Withmock作为解决方案,但我无法让它工作。 Here's the relevant parts of my testing code, which is largely cargo cult code to me, to be honest:这是我的测试代码的相关部分,老实说,这对我来说主要是货物崇拜代码:

import (
    "testing"
    "net/http" // mock
    "code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
    ctrl := gomock.NewController()
    defer ctrl.Finish()
    http.MOCK().SetController(ctrl)
    http.EXPECT().Get(BASE_URL)
    downloader()
    // The rest to be written
}

The test output is following:测试 output 如下:

ERROR: Failed to install '_et/http': exit status 1 output: can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in错误:无法安装'_et/http':退出状态1 output:无法加载package:package _et/http:找到包http(chunked.go)和main(main_mock.go)
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

Is the Withmock a solution to my testing problem? Withmock 可以解决我的测试问题吗? What should I do to get it to work?我应该怎么做才能让它工作?

Personally, I don't use gomock (or any mocking framework for that matter; mocking in Go is very easy without it).就我个人而言,我不使用gomock (或任何与此相关的gomock框架;没有它,在 Go 中进行gomock非常容易)。 I would either pass a dependency to the downloader() function as a parameter, or I would make downloader() a method on a type, and the type can hold the get_page dependency:我要么将依赖项作为参数传递给downloader()函数,要么让downloader()成为一个类型的方法,并且该类型可以保存get_page依赖项:

Method 1: Pass get_page() as a parameter of downloader()方法一:通过get_page()作为downloader()的参数

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}

Main:主要:

func get_page(url string) string { /* ... */ }

func main() {
    downloader(get_page)
}

Test:测试:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}

Method2: Make download() a method of a type Downloader :方法 2:使download()成为Downloader类型的方法:

If you don't want to pass the dependency as a parameter, you could also make get_page() a member of a type, and make download() a method of that type, which can then use get_page :如果您不想将依赖项作为参数传递,您还可以使get_page()成为某个类型的成员,并使download()成为该类型的方法,然后可以使用get_page

type PageGetter func(url string) string

type Downloader struct {
    get_page PageGetter
}

func NewDownloader(pg PageGetter) *Downloader {
    return &Downloader{get_page: pg}
}

func (d *Downloader) download() {
    //...
    content := d.get_page(BASE_URL)
    //...
}

Main:主要:

func get_page(url string) string { /* ... */ }

func main() {
    d := NewDownloader(get_page)
    d.download()
}

Test:测试:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}

If you change your function definition to use a variable instead:如果您更改函数定义以使用变量:

var get_page = func(url string) string {
    ...
}

You can override it in your tests:您可以在测试中覆盖它:

func TestDownloader(t *testing.T) {
    get_page = func(url string) string {
        if url != "expected" {
            t.Fatal("good message")
        }
        return "something"
    }
    downloader()
}

Careful though, your other tests might fail if they test the functionality of the function you override!但是请注意,如果您的其他测试测试您覆盖的功能的功能,它们可能会失败!

The Go authors use this pattern in the Go standard library to insert test hooks into code to make things easier to test: Go 作者使用 Go 标准库中的这种模式将测试钩子插入代码中,使测试更容易:

I'm using a slightly different approach where public struct methods implement interfaces but their logic is limited to just wrapping private (unexported) functions which take those interfaces as parameters.我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。 This gives you the granularity you would need to mock virtually any dependency and yet have a clean API to use from outside your test suite.这为您提供了模拟几乎所有依赖项所需的粒度,并且还拥有一个干净的 API 可以从测试套件外部使用。

To understand this it is imperative to understand that you have access to the unexported methods in your test case (ie from within your _test.go files) so you test those instead of testing the exported ones which have no logic inside beside wrapping.要理解这一点,必须了解您可以访问测试用例中未导出的方法(即从_test.go文件中),因此您可以测试这些方法,而不是测试除了包装之外没有逻辑的导出方法。

To summarize: test the unexported functions instead of testing the exported ones!总结一下:测试未导出的函数而不是测试导出的函数!

Let's make an example.让我们举个例子。 Say that we have a Slack API struct which has two methods:假设我们有一个 Slack API 结构,它有两个方法:

  • the SendMessage method which sends an HTTP request to a Slack webhook将 HTTP 请求发送到 Slack webhook 的SendMessage方法
  • the SendDataSynchronously method which given a slice of strings iterates over them and calls SendMessage for every iteration给出一段字符串的SendDataSynchronously方法迭代它们并为每次迭代调用SendMessage

So in order to test SendDataSynchronously without making an HTTP request each time we would have to mock SendMessage , right?因此,为了在每次我们必须模拟SendMessage时不发出 HTTP 请求的情况下测试SendDataSynchronously ,对吗?

package main

import (
    "fmt"
)

// URI interface
type URI interface {
    GetURL() string
}

// MessageSender interface
type MessageSender interface {
    SendMessage(message string) error
}

// This one is the "object" that our users will call to use this package functionalities
type API struct {
    baseURL  string
    endpoint string
}

// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
    return api.baseURL + api.endpoint
}

// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy 
func (api *API) SendMessage(message string) error {
    return sendMessage(api, message)
}

// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
    return sendDataSynchronously(api, data)
}

// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
    fmt.Println("This function won't get called because we will mock it")
    return nil
}

// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
    for _, text := range data {
        err := sender.SendMessage(text)

        if err != nil {
            return err
        }
    }

    return nil
}

// TEST CASE BELOW

// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
    err      error
    messages []string
}

// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
    // let's store all received messages for later assertions
    sender.messages = append(sender.messages, message)

    return sender.err // return error for later assertions
}

func TestSendsAllMessagesSynchronously() {
    mockedMessages := make([]string, 0)
    sender := mockedSender{nil, mockedMessages}

    messagesToSend := []string{"one", "two", "three"}
    err := sendDataSynchronously(&sender, messagesToSend)

    if err == nil {
        fmt.Println("All good here we expect the error to be nil:", err)
    }

    expectedMessages := fmt.Sprintf("%v", messagesToSend)
    actualMessages := fmt.Sprintf("%v", sender.messages)

    if expectedMessages == actualMessages {
        fmt.Println("Actual messages are as expected:", actualMessages)
    }
}

func main() {
    TestSendsAllMessagesSynchronously()
}

What I like about this approach is that by looking at the unexported methods you can clearly see what the dependencies are.我喜欢这种方法的一点是,通过查看未导出的方法,您可以清楚地看到依赖项是什么。 At the same time the API that you export is a lot cleaner and with less parameters to pass along since the true dependency here is just the parent receiver which is implementing all those interfaces itself.同时,您导出的 API 更清晰,传递的参数更少,因为这里真正的依赖只是父接收器,它本身实现了所有这些接口。 Yet every function is potentially depending only on one part of it (one, maybe two interfaces) which makes refactors a lot easier.然而,每个函数都可能只依赖于它的一部分(一个,也许两个接口),这使得重构容易得多。 It's nice to see how your code is really coupled just by looking at the functions signatures, I think it makes a powerful tool against smelling code.很高兴通过查看函数签名来了解您的代码是如何真正耦合的,我认为它是防止代码异味的强大工具。

To make things easy I put everything into one file to allow you to run the code in the playground here but I suggest you also check out the full example on GitHub, here is the slack.go file and here the slack_test.go .为了方便起见,我将所有内容都放在一个文件中,以便您可以在此处操场上运行代码,但我建议您也查看 GitHub 上的完整示例,这里是slack.go文件,这里是slack_test.go

And here the whole thing.在这里,整件事。

I would do something like,我会做类似的事情,

Main主要

var getPage = get_page
func get_page (...

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := getPage(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

Test测试

func TestDownloader (t *testing.T) {
    origGetPage := getPage
    getPage = mock_get_page
    defer func() {getPage = origGatePage}()
    // The rest to be written
}

// define mock_get_page and rest of the codes
func mock_get_page (....

And I would avoid _ in golang.我会避免在 golang 中使用_ Better use camelCase更好地使用camelCase

Warning: This might inflate executable file size a little bit and cost a little runtime performance.警告:这可能会稍微增加可执行文件的大小并降低运行时性能。 IMO, this would be better if golang has such feature like macro or function decorator. IMO,如果 golang 具有宏或函数装饰器之类的功能,那就更好了。

If you want to mock functions without changing its API, the easiest way is to change the implementation a little bit:如果你想在不改变 API 的情况下模拟函数,最简单的方法是稍微改变实现:

func getPage(url string) string {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

This way we can actually mock one function out of the others.通过这种方式,我们实际上可以从其他函数中模拟一个函数。 For more convenient we can provide such mocking boilerplate:为了更方便,我们可以提供这样的模拟样板:

// download.go
func getPage(url string) string {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

type MockHandler struct {
  GetPage func(url string) string
  Downloader func()
}

var m *MockHandler = new(MockHandler)

func Mock(handler *MockHandler) {
  m = handler
}

In test file:在测试文件中:

// download_test.go
func GetPageMock(url string) string {
  // ...
}

func TestDownloader(t *testing.T) {
  Mock(&MockHandler{
    GetPage: GetPageMock,
  })

  // Test implementation goes here!

  Mock(new(MockHandler)) // Reset mocked functions
}

the simplest way is to set function into a global variable and before test set your custom method最简单的方法是将函数设置为全局变量,然后在测试之前设置自定义方法

// package base36

func GenerateRandomString(length int) string {
    // your real code
}


// package teamManager

var RandomStringGenerator = base36.GenerateRandomString

func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 
    // we are using the global variable
    code = RandomStringGenerator(5)
 
    // your application logic

    return  nil
}

and in your test, you must first mock that global variable在您的测试中,您必须首先模拟该全局变量

    teamManager.RandomStringGenerator = func(length int) string {
        return "some string"
    }
    
   service := &teamManager.TeamManagerService{}
   service.CreateTeam(context.Background())
   // now when we call any method that user teamManager.RandomStringGenerator, it will call our mocked method

another way is to pass RandomStringGenerator as a dependency and store it inside TeamManagerService and use it like this:另一种方法是将 RandomStringGenerator 作为依赖项传递并将其存储在TeamManagerService并像这样使用它:

// package teamManager

type TeamManagerService struct {
   RandomStringGenerator func(length int) string
}

// in this way you don't need to change your main/where this code is used
func NewTeamManagerService() *TeamManagerService {
    return &TeamManagerService{RandomStringGenerator: base36.GenerateRandomString}
}

func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 
    // we are using the struct field variable
    code = m.RandomStringGenerator(5)
 
    // your application logic

    return  nil
}

and in your test, you can use your own custom function并且在您的测试中,您可以使用自己的自定义函数

    myGenerator = func(length int) string {
        return "some string"
    }
    
   service := &teamManager.TeamManagerService{RandomStringGenerator: myGenerator}
   service.CreateTeam(context.Background())

you are using testify like me :D you can do this你像我一样使用作证 :D 你可以这样做

// this is the mock version of the base36 file
package base36_mock

import "github.com/stretchr/testify/mock"

var Mock = mock.Mock{}

func GenerateRandomString(length int) string {
    args := Mock.Called(length)
    return args.String(0)
}

and in your test, you can use your own custom function并且在您的测试中,您可以使用自己的自定义函数

   base36_mock.Mock.On("GenerateRandomString", 5).Return("my expmle code for this test").Once()
    
   service := &teamManager.TeamManagerService{RandomStringGenerator: base36_mock.GenerateRandomString}
   service.CreateTeam(context.Background())

I have been in similar spot.我一直在类似的地方。 I was trying to write unitTest for a function which had numerous clients calling it.我正在尝试为 function 编写 unitTest,它有很多客户调用它。 let me propose 2 options that I explored.让我提出我探索过的 2 个选项。 one of which is already discussed in this thread, I will regardless repeat it for the sake of people searching.其中一个已经在这个线程中讨论过,为了人们搜索的缘故,我将不顾一切地重复它。

Method 1: Declaring function you wanna mock as a Global variable方法 1:将 function 声明为全局变量


one option is declaring a global variable (has some pit falls).一种选择是声明一个全局变量(有一些陷阱)。

eg:例如:

package abc

var getFunction func(s string) (string, error) := http.Get

func get_page(url string) string {
  ....
  resp, err := getFunction(url)
  ....
}

func downloader() {
  .....
}

and the test func will be as follows:测试函数如下:

package abc

func testFunction(t *testing.T) {
  actualFunction := getFunction
  getFunction := func(s string) (string, error) { 
     //mock implementation 
  }
  defer getFunction = actualFunction
  .....
  //your test
  ......
}

NOTE: test and actual implementation are in the same package.注意:测试和实际实现在同一个 package。

there are some restrictions with above method thought!以上方法思想有一些限制!

  1. running parallel tests is not possible due to risk of race conditions.由于竞争条件的风险,不可能运行并行测试。
  2. by making function a variable, we are inducing a small risk of reference getting modified by future developers working in same package.通过使 function 成为一个变量,我们降低了参考被未来在同一个 package 中工作的开发人员修改的风险。

Method 2: Creating a wrapped function方法 2:创建一个包装好的 function


another method is to pass along the methods you want to mock as arguments to the function to enable testability.另一种方法是将要模拟的方法作为 arguments 传递给 function 以启用可测试性。 In my case, I already had numerous clients calling this method and thus, I wanted to avoid violating the existing contracts.就我而言,我已经有许多客户调用此方法,因此,我想避免违反现有合同。 so, I ended up creating a wrapped function.所以,我最终创建了一个包装好的 function。

eg:例如:

package abc

type getOperation func(s string) (string, error)

func get_page(url string, op getOperation) string {
  ....
  resp, err := op(url)
  ....
}

//contains only 2 lines of code
func downloader(get httpGet) {
  op := http.Get
  content := wrappedDownloader(get, op)
}

//wraps all the logic that was initially in downloader()
func wrappedDownloader(get httpGet, op getOperation) {
  ....
  content := get_page(BASE_URL, op)
  ....
}

now for testing the actual logic, you will test calls to wrappedDownloader instead of Downloader and you would pass it a mocked getOperation .现在为了测试实际逻辑,您将测试对wrappedDownloader而不是Downloader的调用,并且您将向它传递一个模拟的getOperation this is allow you to test all the business logic while not violating your API contract with current clients of the method.这允许您测试所有业务逻辑,同时不违反您与该方法的当前客户的 API 合同。

Considering unit test is the domain of this question, highly recommend you to use monkey .考虑到单元测试是这个问题的领域,强烈建议您使用monkey This Package make you to mock test without changing your original source code.该软件包使您可以在不更改原始源代码的情况下进行模拟测试。 Compare to other answer, it's more "non-intrusive".与其他答案相比,它更“非侵入性”。

main主要的

type AA struct {
 //...
}
func (a *AA) OriginalFunc() {
//...
}

mock test模拟考试

var a *AA

func NewFunc(a *AA) {
 //...
}

monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)

Bad side is:不好的一面是:

  • Reminded by Dave.C, This method is unsafe. Dave.C 提醒,这种方法是不安全的。 So don't use it outside of unit test.所以不要在单元测试之外使用它。
  • Is non-idiomatic Go.是非惯用的 Go。

Good side is:好的一面是:

  • Is non-intrusive.是非侵入性的。 Make you do things without changing the main code.让你在不改变主代码的情况下做事。 Like Thomas said.就像托马斯说的。
  • Make you change behavior of package (maybe provided by third party) with least code.让你用最少的代码改变包(可能由第三方提供)的行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM