英文:
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 vias.tp
. You could also make it an input parameter of the method.github.NewClient()
andclient.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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论