在golang中模拟外部结构依赖

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

Mocking external struct dependencies in golang

问题

我在使用golang编写可测试代码方面遇到了困难。我理解接口的重要性以及在测试中的使用,但我还没有找到如何模拟/测试外部结构依赖的方法。

举个例子,我编写了以下代码,模拟了一个用于在GitHub上创建拉取请求的包装器。

type GitHubService interface {

}

type gitHubService struct {
    CreatePullRequest(...) (PullRequest,error)
}

func (s gitHubService) CreatePullRequest(...) (PullRequest,error) {
  tp := github.BasicAuthTransport{
      Username: strings.TrimSpace(/*.....*/),
      Password: strings.TrimSpace(/*.....*/),
    }

    client := github.NewClient(tp.Client())
  pr,err := client.Repositories.CreatePullRequest(...)

  ...
}

func TestPullRequest(t *testing.T) {
  service := gitHubService{}
  pr,err := service.CreatePullRequest(...)
  ...
}

如果我要为GitHubService.CreatePullRequest(...)编写单元测试,我希望模拟对client.Repositories.CreatePullRequest(...)的调用,甚至可能是github.NewClient(...),以返回我可以控制的模拟实现。

使用诸如gomock之类的工具,似乎无法处理结构体和包函数。

处理这个问题的惯用方法是什么?我非常熟悉控制反转以及不同的模式,如依赖注入和服务定位器,但我已经听过很多次这不是惯用的做法。

英文:

I am having a hard time figuring out an idiomatic way of writing testable code in golang. I understand the importance of interfaces and their use in testing, but I haven't figured out how to mock/test external struct dependencies.

As an example, I have written the following which simulates a wrapper for creating a pull request on GitHub.

type GitHubService interface {

}

type gitHubService struct {
    CreatePullRequest(...) (PullRequest,error)
}

func (s gitHubService) CreatePullRequest(...) (PullRequest,error) {
  tp := github.BasicAuthTransport{
      Username: strings.TrimSpace(/*.....*/),
      Password: strings.TrimSpace(/*.....*/),
    }

    client := github.NewClient(tp.Client())
  pr,err := client.Repositories.CreatePullRequest(...)

  ...
}

func TestPullRequest(t *testing.T) {
  service := gitHubService{}
  pr,err := service.CreatePullRequest(...)
  ...
}

If I was writing a unit test for GitHubService.CreatePullRequest(...) I would want to mock the call to client.Repositories.CreatePullRequest(...) and probably even github.NewClient(...) to return mock implementations that I can control.

With tools such as gomock it seems that you are out of luck with structs and package functions.

What is the idiomatic way to handle this? I am very familiary with Inversion of Control and the different patterns such as Dependency Injection and Service Locator, but I have heard countless times that this is not idiomatic.

答案1

得分: 4

Go的一个重要设计特点是解耦观看Bill Kennedy关于这个主题的精彩演讲)。在你的方法中存在一些依赖关系,这些依赖关系可以解耦。这种耦合的方法使得它不太容易进行测试。

需要重构的内容:

  • tp := github.BasicAuthTransport:你不应该在方法内部初始化授权。它应该作为参数移动到你的gitHubService中。在方法调用内部,你可以通过s.tp访问它。你也可以将其作为方法的输入参数。
  • github.NewClient()client.Repositories.CreatePullRequest(...),只需阅读Peter Bourgon的关于Golang最佳实践的文章明确依赖关系!。另一种方法是创建一个包含所有被调用函数的接口。这个接口应该作为方法的输入。

在你的代码解耦之后,你可以很容易地模拟一切。如果你将接口作为输入,你只需创建一个实现该接口的模拟结构体。如果你明确依赖关系,你可以覆盖它们。在最后一种情况下,存储调用值的代码不太干净,但也可以工作。Go的惯用方式是使用接口。

英文:

One important design feature of Go is decoupling (Watch this great talk from Bill Kennedy about that topic). Inside your method there are some dependencies, which could be decoupled. This coupled method makes it not really testable.

Thing you should refactor:

  • tp := github.BasicAuthTransport: you should not initialize the authorization inside of your method. It should move into your gitHubService as a parameter. Inside your method call you can access ist via s.tp. You could also make it an input parameter of the method.
  • github.NewClient() and client.Repositories.CreatePullRequest(...) just read about the golang best practices from Peter Bourgon Make dependencies explicit!. The alternative is to create an interface, which contains all the called functions. This interface should be an input to your method.

After your code is decoupled you can mock everything very easy. If you use interfaces as an input you can just create a mock struct, which implements the interface. If you make the dependencies explicit you can overwrite them. In the last case the code for storing the values of the calls is not so clean, but it also works. The idiomatic go way is to use interfaces.

答案2

得分: 3

我遇到了类似的问题,看起来,你唯一的选择是在中间添加另一层,该层将调用你的“无法模拟”的客户端。

例如,为了模拟 govmi 客户端(用于 golang 的 vmware 客户端 SDK),我不得不创建一个名为“myCustomClient”的客户端,其中包含用于调用 govmi.Client.AnyMethod 的接口和结构体。

然后,我可以为“myCustomClient”生成模拟。

mockgen -source myCustomClient.go -package myPackage -destination myCustomClientMock.go

你可以通过以下方式安装它:got get github.com/golang/mock

英文:

I had a similar problem and looks like, the only option you have is to have another layer in between which would call your "unmockable" clients.

For e.g. for mocking the govmi clients (vmware clients sdk for golang), I had to have a "myCustomClient" having interfaces and structs to make calls to govmi.Client.AnyMethod..

I could then generate mocks for "myCustomClient".

mockgen -source myCustomClient.go -package myPackage -destination myCustomClientMock.go

You can install it by: got get github.com/golang/mock

huangapple
  • 本文由 发表于 2017年2月24日 01:45:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/42422747.html
匿名

发表评论

匿名网友

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

确定