3

Let's say I have this code and I want to create a test for Foo()

The important part it Foo makes a call to Bar

package main

type MyInterface interface {
    Foo() error
    Bar() error
}

type MyStruct struct {
}

func NewMyStruct() MyInterface{
    return &MyStruct{}
}

func (m *MyStruct) Foo() error {
    // do something
    m.Bar()
    // do something else
    return nil
}

func (m *MyStruct) Bar() error {
    // do something
    return nil
}

Is it possible to create a test for this where I can mock the behaviour of Bar before I run Foo? or am I doing something fundamentally wrong?

I can see that if I extracted Bar into its own service I would mock it that way but that also doesn't feel right.

Any insights or links to documentation would be great.

Mexicoder
  • 517
  • 1
  • 6
  • 18
  • You can only mock types, not methods; if you need to mock `Bar` for this test, it needs to be in a separate type. Are you sure you need to mock it? – Adrian Feb 15 '23 at 15:35
  • This is a very watered-down version of what my project does. I have a service that makes an API call, I want to test this function (Foo) but this function calls a function to create the URL (Bar), this function is also in the same interface. – Mexicoder Feb 15 '23 at 15:43
  • 2
    Don't mock, make actual calls. – Volker Feb 15 '23 at 16:04
  • 2
    @Volker Even if the actual call launches missiles? – jub0bs Feb 15 '23 at 16:18

1 Answers1

3

You should be able to achieve what you need with a couple of changes. First, let me present the code and then I'll walk you through all of the relevant changes.

main.go file

package main

type MyInterface interface {
    Foo() error
    Bar() error
}

type MyStruct struct {
    DS MyInterface
}

// here you've to depend upon an interface and return a pointer to a struct
func NewMyStruct(ds MyInterface) *MyStruct {
    return &MyStruct{
        DS: ds,
    }
}

func (m *MyStruct) Foo() error {
    // do something
    m.DS.Bar()
    // do something else
    return nil
}

func (m *MyStruct) Bar() error {
    // do something
    return nil
}

func main() {}

Here, I changed a couple of things in order to be able to successfully mock our dependencies. Let me recap them:

  1. The MyStruct has a dependency of type MyInterface
  2. The function NewMyStruct accepts the interface as parameter and returns a pointer to the MyStruct struct
  3. In the Foo method, we're going to rely on the DS field of our pointer receiver type instance

Now, let's switch to the test file.

main_test.go file

package main

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

// 1. declare mock
type myStructMock struct {
    mock.Mock
}

// 2. implement the interface
func (m *myStructMock) Foo() error {
    args := m.Called()
    return args.Error(0)
}

func (m *myStructMock) Bar() error {
    args := m.Called()
    return args.Error(0)
}

func TestFoo(t *testing.T) {
    // 3. instantiate/setup mock
    myStructMock := new(myStructMock)
    myStructMock.On("Bar").Return(nil).Times(1)

    sut := NewMyStruct(myStructMock)
    err := sut.Foo()

    // 4. check that all expectations were met on the mock
    assert.Nil(t, err)
    assert.True(t, myStructMock.AssertExpectations(t))
}

Here, I found it best to add comments within the code to give you a chronological order of what's going on. The idea is that the real system tested (e.g. the sut variable) is relying on mock instead of actual implementations. Thanks to the method Times, we make sure that the Bar method is called once.

I always used this approach when it comes to testing production code and I find it pretty flexible and amazing!
Let me know if this helps you or if you still need something else, thanks!

ossan
  • 1,665
  • 4
  • 10
  • 1
    thanks, this helps a lot. it took me a bit to understand the composition but I can see why you like this approach. also, does that mean your code instantiation usually looks something like this: 's := NewMyStruct(new(MyStruct))' just curious b/c it does feel a little weird – Mexicoder Feb 15 '23 at 16:52
  • Will this still work if `MyStruct` has other fields than the interface parameter? – Inian Mar 29 '23 at 16:32
  • Hey @Inian, this will work even if there is another field in the `MyStruct` struct or even if the `MyStruct` implements a method not belonging to the `MyInterface` interface. – ossan Mar 30 '23 at 06:45