Jose Juan MontielJose Juan Montiel

Motivation

On this occasion, I will comment on my advances in the world of testing with GoLang. To do this, with the help of the following code I will comment on some concepts that I have learned.

Interfaces

This is the way to define an interface in Go.

type Doer interface {
    DoSomething(int, string) error
}

For a class to implement an interface, it is only necessary to implement all the methods of the interface.

type doerImpl struct {              (1)
}

func NewDoerService() Doer {        (2)
	return &doerImpl{}              (3)
}

func (d *doerImpl) DoSomething(numero int, cadena string) error {   (4)
	fmt.Println("Calling DoSomething method")
	fmt.Println(cadena)                                 (5)
	return errors.New("error")                          (6)
}
1 Every class needs a structure.
2 This function is the constructor of the implementation, it returns the interface.
3 Thus we return the reference to the structure of this implementation.
4 This would be the method we are going to implement.
5 The implementation prints the input parameter.
6 Returns an error class.

Now suppose we have another class where we make use of that interface implementation.

type User struct {
	Doer doer.Doer                  (1)
}

func NewUser(d doer.Doer) *User {   (2)
	return &User{d}                 (3)
}

func (u *User) Use() error {
	fmt.Println("Calling Use method")
	return u.Doer.DoSomething(123, "Hello GoMock")  (4)
}
1 In this case, when declaring the structure of the class, we define an attribute of the type of the interface
2 In the constructor of this class, we passed an instance of the implementation of the interface
3 This is where we spent it.
4 This function makes use of the function that we have implemented in the interface.

Now consider the following main that will instantiate the implementation and then pass it to the function that makes use of the interface.

func main() {
	newDoerImpl := doer.NewDoerService()        (1)
	_ = newDoerImpl.DoSomething(1, "hola")      (2)

	newUser := user.NewUser(newDoerImpl)        (3)
	newUser.Use()                               (4)
}
1 Using the constructor, we instantiate the implementation
2 Now we call the function of the implementation
3 If we take that instance and pass it to the constructor of the new class
4 We can make use of the function, which will make use of the implementation class

Now suppose we want to test the Use() function but we do not want DoSomething to be called.

For this we are going to use the libreria de mock en go. This library like other similar ones in other languages allow us to mock the objects that we do not want to test, in this case our objective is to mock the implementation of the interface to be able to test the Use method.

Note that to generate this mock class, we must indicate an interface on which the implementation that we want to mock has been built, in this case Doer, and gomock generates a class that allows us to instrumentalize the class to be mocked.

func TestUseReturnsErrorFromDo(t *testing.T) {
	fmt.Println("TestUseReturnsErrorFromDo")

	mockCtrl := gomock.NewController(t)             (1)
	defer mockCtrl.Finish()

	dummyError := errors.New("dummy error")
	mockDoer := mocks.NewMockDoer(mockCtrl)         (3)
	testUser := &user.User{Doer: mockDoer}          (2)

	mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(dummyError).Times(1)  (4)

	err := testUser.Use()                           (5)

	if err != dummyError {
		t.Fail()
	}
}
1 Initialize the mock library.
2 We instantiate the class to be tested, passing a mock instead of the interface
3 We instantiate the mock class, which we have passed as a parameter in the constuctor of the class to be tested.
4 Instrumentalize the mock, in this case we hope that when the test calls the class mocked with those parameters, the answer will be as expected.
5 We launch the class to test.

Note that if we launch the test in verbose mode we will not see the print of the implementation of the interface, because it will not have been called, it will be the instrumentation that will be responsible for returning the result.

For a next article, note that if the interface that we want to mock is within a project that uses go dep, we can have problems with the package of the import and that indicate vendor.

But that will be for another day.