Skip to main content

Concurrency With Gomock

I promise this gonna be quick one (and also helpful)

Photo by Nicole Wreyford on Unsplash

Okay, so still with Go unit testing and mocking. Have you ever encountered when you want to test a function and that function calls a dependency in other goroutines and makes your test inconsistent?

You do? Okay great let’s solve it!

Okay, to solve this problem, you gonna need:

  1. Go’s sync.WaitGroup and
  2. GoMock’s .Do method.

Let’s say you have this service

package service

import (
	"context"
	"fmt"
)

type Repository interface {
	DoSomething() error
}

type Service struct {
	repository Repository
}

func NewService(r Repository) *Service {
	return &Service{repository: r}
}

func (s *Service) DoSomething(ctx context.Context) error {
	go func() {
		if err := s.repository.DoSomething(); err != nil {
			fmt.Println("error happening!!!")
		}
	}()
	return nil
}

to this this part of the function, you gonna need to do something like this…

package service_test

import (
	"context"
	"example/service"
	"example/service/mock"
	"fmt"
	"sync"
	"testing"

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

func TestDoSomething(t *testing.T) {
	t.Run("return nil", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		var wg sync.WaitGroup
		ctx := context.Background()
		mockRepository := mock.NewMockRepository(ctrl)

		// !!! IMPORTANT NOTE !!!
		// The function (or anonymous function) that you passed
		// into `.Do` method should have exact signature with
		// the method that you called in other goroutines.
		wg.Add(1)
		mockRepository.
			EXPECT().
			DoSomething().
			Times(1).
			Return(nil).
			Do(func() {
				defer wg.Done()
			})

		service := service.NewService(mockRepository)
		actual := service.DoSomething(ctx)

		wg.Wait()
		assert.NoError(t, actual)
	})

	t.Run("return an error", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		var wg sync.WaitGroup
		ctx := context.Background()
		mockRepository := mock.NewMockRepository(ctrl)

		// !!! IMPORTANT NOTE !!!
		// The function (or anonymous function) that you passed
		// into `.Do` method should have exact signature with
		// the method that you called in other goroutines.
		wg.Add(1)
		mockRepository.
			EXPECT().
			DoSomething().
			Times(1).
			Return(fmt.Errorf("shit happened!")).
			Do(func() interface{} {
				defer wg.Done()
				return fmt.Errorf("shit happened!")
			})

		service := service.NewService(mockRepository)
		actual := service.DoSomething(ctx)

		wg.Wait()
		assert.NoError(t, actual)
	})
}

Thank you everyone!

PS: if you want to read more (or understand more) about this, you can go read this good Medium article: Unit Testing and mock calls inside goroutines by Jeffy Mathew

comments powered by Disqus