如何使用反射实现或模拟一个Go接口?

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

How to implement or mock a Go interface with reflection?

问题

我想使用反射来实现Go接口以生成模拟和存根。但是,如果我查看reflect包,我不知道如何做到这一点(也许不可能)。

例如:测试一个函数是否在ResponseWriter上调用了WriteHeader(404)

type ResponseWriterMock struct {
  status int
}

func (*ResponseWriterMock) Header() Header {}
func (*ResponseWriterMock) Write([]byte) (i int, e error) {}
func (m *ResponseWriterMock) WriteHeader(status int) {
  m.status = status
}

responseWriterMock := new(ResponseWriterMock)

funcToTest(responseWriterMock)

if responseWriterMock.status != 404 {
  // 报告错误
}

使用RhinoMocks(C#),我会这样写:

var responseWriterMock = MockRepository.GenerateMock<ResponseWriter>();

funcToTest(responseWriterMock);

responseWriterMock.AssertWasCalled(rw => rw.WriteHeader(404));

如何使用反射实现Go接口?

补充说明

似乎目前不可能。

英文:

I want to implement Go interfaces with reflection to generate mocks and stubs. But if I look at the reflect package, I get no idea how to do it (maybe it is not possible).

Example: Testing that a func calls WriteHeader(404) on ResponseWriter:

type ResponseWriterMock struct {                              //
  status int                                                  //
}                                                             // How to replace
func (*ResponseWriterMock) Header() Header {}                 // this block with 
func (*ResponseWriterMock) Write([]byte) (i int, e error) {}  // a reflectivly 
func (m *ResponseWriterMock) WriteHeader(status int) {        // generated mock? 
  m.status = status                                           //  
}                                                             //
responseWriterMock := new(ResponseWriterMock)

funcToTest(responseWriterMock)

if responseWriterMock.status != 404 {
	// report error
}

With RhinoMocks (C#) I would write it like this:

var responseWriterMock = MockRepository.GenerateMock&lt;ResponseWriter&gt;();

funcToTest(responseWriterMock);

responseWriterMock.AssertWasCalled(rw =&gt; rw.WriteHeader(404));

How can I implement Go interfaces with reflection?

Addendum

Seems like it is not possible today.

答案1

得分: 4

很不幸,由于语言的静态特性,反射包的功能有些有限,没有办法在运行时创建动态对象。可能永远都不会有这个可能性。

我可以提出几个解决方案 - 都涉及在设计时创建任何存根/模拟。

  1. 手动编写模拟。

    我见过有人这样做,但一旦代码库变得更大,这种方式很快就变得难以忍受。

    总的来说,思路是编写一个实现你的接口的结构。在每个方法中,你可以添加代码来增加一些计数器(以跟踪调用次数),并经常返回一个已经硬编码或作为结构配置提供的值。

  2. 使用 testify 工具包。

    在撰写本文时,这似乎是目前最受欢迎的提供模拟支持的项目。然而,由于该项目提供其他功能,对于喜欢的程度有多少是由于模拟能力本身,这是值得怀疑的。

    我没有使用过这个工具包,无法提供详细的分析,无法判断它是否好用。

    从 README 来看,它采用了与手动模拟相似的方法(上面解释过)。它提供了一些辅助代码,但假方法仍然需要手动输入。然后,通过指定字符串来指定测试中的存根,这与 Go 语言的静态特性相违背,可能对某些用户来说是个问题。

  3. 使用官方的 mock 工具。

    自从我上次写到它以来,这个工具似乎已经变得越来越受欢迎。此外,作为官方的 Go 项目,可以预期该工具将迅速采用任何新的 Go 特性。它对 Go 模块的支持就是一个很好的例子,其他工具落后于它。

    它采用了一种特定的模拟方法。你需要在测试代码的开头指定测试期望。在测试结束时,框架会检查是否满足了所有的期望。意外的调用会导致框架测试失败。

    如果你正在使用像 Ginkgo 这样的自定义测试框架,可能会很难进行清洁的集成。在我的项目中,我编写了一些辅助函数来弥合这个差距。

    虽然它会为你生成假方法,但它生成的断言和期望方法接受 interface{} 类型。这允许框架提供更高级的参数匹配器,但也意味着你需要确保传递正确数量和正确顺序的参数。

    根据我的经验,使用这个工具编写的测试往往更大,并且有时更复杂。好处是,它们通常更彻底地测试事物,并且更具表达性。

  4. 使用 counterfeiter 工具。

    这个工具在 Cloud Foundry 生态系统中被广泛使用,并且已经证明是一个可行的选择。

    它允许你跟踪给定方法被调用的次数、用于调用方法的参数,并且可以伪造整个方法的自定义实现或指定要返回的结果值。

    它之所以是一个好选择,是因为它生成的伪造方法非常明确。开发人员不需要使用字符串名称来指定方法,也不需要记住参数的顺序和数量。生成的辅助方法与原始方法非常相似,具有正确类型和顺序的参数。


我上面提供的一些选项需要你运行一个命令行工具来为你的模拟生成源代码。由于需要模拟的接口数量可能会迅速增长,你可以使用一个很酷的技巧来跟踪所有内容。

你可以利用 Go 中的 go:generate 功能,轻松地为所有的存根/伪造生成代码,而无需记住任何命令参数或手动为每个接口运行工具。

你只需要在包含你的接口的文件中添加适当的 go:generate 注释即可。

mock

//go:generate mockgen -source person.go

type Person interface {
  Name() string
  Age() int
}

counterfeiter

//go:generate counterfeiter ./ Person

type Person interface {
  Name() string
  Age() int
}

然后,在项目的根目录中运行 go generate ./... 命令,所有的存根/伪造将会为你重新生成。如果你正在与多个协作者一起工作,这可能会是一个救命稻草。

英文:

Unfortunately, due to the static nature of the language, the reflection package is a bit limited and there is no way to create dynamic objects at runtime. It might never be possible.

I can propose a few solutions - all involving the creation of any stubs/mocks at design time.

  1. Write your mocks manually.

I've seen people go down that road and it quickly becomes unbearable once the codebase becomes larger.

In general, the idea is to write a structure that implements your interface. In each method you add code that increments some counter (in order to track invocations) and often returns a value that has been hardcoded or provided as a configuration to the structure.

  1. Use the testify toolkit.

At the time of writing, this appears to be the most popular project out there providing mocking support. However, since the project provides other features, it is questionable how many of the likes are due to the mocking capability itself.

I have not used this toolkit and cannot provide detailed analysis as to whether it is any good.

Looking at the README, it appears that it takes an approach very similar to the manual mocking one (explained above). It provides some helper code on top but the fake methods still need to be typed manually. Stubbing in the tests is then done by specifying the method via a string, which is going against the static nature of the Go language and might be a problem for some users.

  1. Use the official mock tool from Go.

This tool seems to have gained popularity since I last wrote about it. Furthermore, as on official Go project, one can expect that this tool will quickly adopt any new Go features. The support it has for go modules is a good example, where other tools lag behind.

It does take a specific approach to mocking. You need to specify your test expectations at the beginning of the test code. At the end of the test, the framework checks whether all expectations have been met. Unexpected calls cause the framework to fail the test.

If you are using a custom testing framework like Ginkgo, it might be tricky to cleanly integrate. In my projects, I have written helper functions to bridge the gap.

While it does generate the fake methods for you, the assertion and expectation methods it generates take interface{} types. This allows the framework to provide more advanced argument matchers but also means that you need to ensure you pass the correct number of arguments and in the correct order.

In my experience, tests written with this tool tend to be bigger in side and at times more complicated to read. On the plus side, they often test things more thoroughly and are more expressive.

  1. Use the counterfeiter tool.

This tool has been used a lot in the Cloud Foundry ecosystem and has proven itself as a viable option.

It allows you to track the number of times a given method has been called, the arguments that were used to call the method, and to fake either the whole method with a custom implementation or specify the result values to be returned.

What makes it a good choice is that it produces very explicit fakes. Developers don't need to use string names to specify a method, nor do they need to remember the order and number of arguments. The generated helper methods mimic closely the original ones, with arguments of the correct type and order.


Some of the options I provided above require that you run a command line tool to generate the source code for your mocks. As the number of interfaces that need to be mocked can quickly grow, there is a cool trick you could use to keep track of everything.

You can take benefit of the go:generate functionality in Go to easily generate all your stubs/fakes, without having to recall any command parameters or to manually run the tool for each interface.

All you need to do is to add the proper go:generate comment in the file that contains your interface.

mock

//go:generate mockgen -source person.go

type Person interface {
  Name() string
  Age() int
}

counterfeiter

//go:generate counterfeiter ./ Person

type Person interface {
  Name() string
  Age() int
}

You can then run the go generate ./... command in the root of your project and all the stubs/fakes will be regenerated for you. This can be a lifesaver if you are working on a project with multiple collaborators.

答案2

得分: 2

根据我所了解和这个相关问题的答案,在运行时创建新类型是不可能的。

你可以尝试使用go-eval,它应该支持在其宇宙中定义新类型。

英文:

To the best of my knowledge and the answers of this related question, it is not possible to create new types at runtime.

You may want to try the go-eval package which should support defining new types in it's universe.

huangapple
  • 本文由 发表于 2012年10月18日 16:33:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/12950346.html
匿名

发表评论

匿名网友

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

确定