Reputation: 2565
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
}
Upvotes: 0
Views: 230
Reputation: 3815
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.
Upvotes: 2