如何在Go中模拟一个方法?

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

How can I mock a method on Go?

问题

假设我有一个类型 foo,其中有一个方法 largerInt() 调用了 largeInt()。我想测试 largerInt(),所以我需要模拟 largeInt(),以避免可能的副作用。

然而,我无法成功地进行模拟。使用接口和组合,我可以模拟 largeInt(),但在 largerInt() 内部,似乎无法进行模拟,因为在调用它时,没有对包装类型的引用。

你有什么办法可以解决这个问题吗?下面是我创建的一个代码片段来说明这个问题:

package main

import (
	"fmt"
)

type foo struct {
}

type mockFoo struct {
	*foo
}

type MyInterface interface {
	largeInt() int
}

func standaloneLargerInt(obj MyInterface) int {
	return obj.largeInt() + 10
}

func (this *foo) largeInt() int {
	return 42
}

func (this *mockFoo) largeInt() int {
	return 43
}

func (this *foo) largerInt() int {
	return this.largeInt() + 10
}

func main() {
	myA := &foo{}
	myB := &mockFoo{}
	fmt.Printf("%s\n", standaloneLargerInt(myA)) // 52
	fmt.Printf("%s\n", standaloneLargerInt(myB)) // 53

	fmt.Printf("%s\n", myA.largerInt()) // 52
	fmt.Printf("%s\n", myB.largerInt()) // 52

}

谢谢!

英文:

Suppose I have a type foo with a method largerInt() that calls largeInt(). I want to test largerInt(), so I need to mock largeInt(), because of possible side effects.

I'm failing to do so, however. Using interfaces and composition, I can mock largeInt(), but inside largerInt(), it seems unmockable, as when calling it, there are no references to the wrapper type.

Any idea on how to do it? Below is a snippet I created to illustrate the problem

Thanks!

package main

import (
	"fmt"
)

type foo struct {
}

type mockFoo struct {
	*foo
}

type MyInterface interface {
	largeInt() int
}

func standaloneLargerInt(obj MyInterface) int {
	return obj.largeInt() + 10
}

func (this *foo) largeInt() int {
	return 42
}

func (this *mockFoo) largeInt() int {
	return 43
}

func (this *foo) largerInt() int {
	return this.largeInt() + 10
}

func main() {
	myA := &foo{}
	myB := &mockFoo{}
	fmt.Printf("%s\n", standaloneLargerInt(myA)) // 52
	fmt.Printf("%s\n", standaloneLargerInt(myB)) // 53

	fmt.Printf("%s\n", myA.largerInt()) // 52
	fmt.Printf("%s\n", myB.largerInt()) // 52

}

答案1

得分: 2

由于Go语言没有任何形式的继承,你可能无法完全得到你想要的结果。然而,有一些替代方法可以实现这种关系,我认为这些方法已经足够好用了。

首先,让我们深入了解一下你的代码中到底发生了什么。你可能已经了解其中大部分内容,但是重新阐述一下可能会更加明显:

当你最初声明mockFoo时:

type mockFoo struct {
    *foo
}

这并不会在这两种类型之间创建任何真正的关系。它所做的是将foo的方法提升到mockFoo中。这意味着任何在foo上的方法,如果在mockFoo上没有定义,都会被添加到后者中。所以myB.largerInt()myB.foo.largerInt()是完全相同的调用;只是没有从foo->mockFoo的真正关系可以像你所描述的那样使用。

这是有意为之的——组合与继承相对立的思想的一部分,它通过限制子组件之间的相互作用,使得对子组件行为的推理变得更加容易。


那么,你现在该怎么办呢?我认为传统的模拟在Go中不太适用,但是类似的原则是适用的。与其尝试通过创建一个包装器来“子类化”foo,不如将所有被模拟目标的方法隔离到一个独立的接口中。

但是:如果你想测试foo上没有副作用的方法怎么办?你已经找到了一种替代方法:将所有你希望测试的功能放在单独的静态方法中。然后,foo可以将其所有静态行为委托给它们,它们将非常容易进行测试。

还有其他更接近你所描述的结构的选项。例如,你可以颠倒mockFoofoo之间的关系:

type foo struct {
    fooMethods
}

type fooMethods interface {
    largeInt() int
}

func (this *foo) largerInt() int {
    return this.largeInt() + 10
}

type fooMethodsStd struct{}

func (this *fooMethodsStd) largeInt() int {
    return 42
}

var defaultFooMethods = &fooMethodsStd{}

type fooMethodsMock struct{}

func (this *fooMethodsMock) largeInt() int {
    return 43
}

var mockedFooMethods = &fooMethodsMock{}

func main() {
    normal := foo{defaultFooMethods}
    mocked := foo{mockedFooMethods}

    fmt.Println(normal.largerInt()) // 52
    fmt.Println(mocked.largerInt()) // 53
}

然后,你可以通过"插入"结构的有状态组件来管理它,而不是通过继承来管理。你可以在运行时将其设置为defaultFooMethods,并在测试时使用模拟版本。由于结构体中缺少默认值,这可能有点麻烦,但是它是可行的。


对于那些更喜欢组合而不是继承的人来说,这是一种特性,而不是错误。对具有副作用的方法进行任意模拟是一项混乱的任务——程序本身没有任何提示表明哪些是有状态的,必须被隔离,哪些不是。事先强制澄清关系可能需要更多的工作,但确实使代码的交互和行为更加明显。

英文:

Since Go does not have any form of inheritance, you probably won't be able to get exactly what you're looking for. However, there are some alternate approaches to these kinds of relationships that I find work well enough.

But first, let's take a deeper look into what, exactly is going on in your code. You probably already know most of this, but restating it might make the behavior a little more obvious:

When you initially declare mockFoo:

type mockFoo struct {
    *foo
}

This doesn't create any real relationship between the two types. What it does do is promote methods from foo to mockFoo. That means any method on foo that is not also on mockFoo will be added to the latter. So that means myB.largerInt() and myB.foo.largerInt() are identical invocations; there's just no real relationship from foo->mockFoo that can be used like you laid out.

This is intentional - part of the idea of composition opposed to inheritance is that it makes it considerably easier to reason about the behavior of subcomponents by limiting how they interact.


So: where does that leave you? I'd say that conventional mocking won't port very well with Go, but similar principles will. Rather than trying to "subclass" foo by creating a wrapper, you have to isolate all the methods of the mocked target into a distinct interface.

But: what if you want to test methods on foo that have no side effects? You've already hit upon one alternative to this: put all the functionality you wish to test in separate static methods. Then foo can delegate all its static behavior to them, and they will be quite easy to test.

There are other options that are more akin to the structure you laid out. For instance, you could invert the relationship between mockFoo and foo:

type foo struct {
	fooMethods
}

type fooMethods interface {
	largeInt() int
}

func (this *foo) largerInt() int {
	return this.largeInt() + 10
}

type fooMethodsStd struct{}

func (this *fooMethodsStd) largeInt() int {
	return 42
}

var defaultFooMethods = &fooMethodsStd{}

type fooMethodsMock struct{}

func (this *fooMethodsMock) largeInt() int {
	return 43
}

var mockedFooMethods = &fooMethodsMock{}

func main() {
	normal := foo{defaultFooMethods}
	mocked := foo{mockedFooMethods}

	fmt.Println(normal.largerInt()) // 52
	fmt.Println(mocked.largerInt()) // 53
}

Then, you "plug" the stateful component of the struct rather than manage it through inheritance. You'd then set if to defaultFooMethods during runtime, and use a mocked version for testing. This is a little annoying due to the lack of default values in structs, but it works.


For those who favor composition over inheritance, this is a feature, not a bug. Arbitrarily mocking methods with side effects is a messy business - there's nothing in the program itself to suggest what is stateful and must be isolated, and what is not. Forcing the clarification of the relationship beforehand can take some more work, but does make the interactions and behavior of the code more obvious.

huangapple
  • 本文由 发表于 2015年11月27日 03:33:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/33945873.html
匿名

发表评论

匿名网友

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

确定