Testing a function that calls a goroutine
Tags: go, tdd, concurrency, unittest
Published: 2020-07-19 03:47:10

"If you are using a third party framework to generate SQL your TDD tests should check that the framework is being called correctly." - [Uncle Bob](https://twitter.com/unclebobmartin/status/1234840690468827136)

## Problem
Creating unit tests where there is concurrency can become complicated if we do not take special care. In this article, we are going to show a simple scenario, but it could take us a few minutes to figure out how to do it. It occurs when we want to create an unit test for a function that internally calls another library asynchronously.

Let's look at the model of this scenario.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/6dwrquoa1ap6wssdkr46.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/34d3oeis1d2glp2twhbe.png)

now take a look at the notification unit test

We created a `basicService` with the mock notifier and called the `Add()` function to verify that the expected message has been sent to the `emailNotifierMock`.
```go
import (
	"context"
	"testing"

	"github.com/group/project/pkg/employeeapp"
	"github.com/stretchr/testify/assert"
)

func TestEmailNotification(t *testing.T) {
	// GIVEN
	newEmployee := employeeapp.Employee{
		FirstName: "Daenerys",
		LastName:  "Targaryen",
		Email:     "someone@somewhere.com",
		Salary:    25000,
	}
	expectedMessage := employeeapp.Message{
		Subject: "Hey",
		To:      "someone@somewhere.com",
		From:    "anybody@somewhere.com",
		Body:    "Good morning there",
	}
	ctx := context.TODO()
	emailNotifier := NewEmailNotifierMock()
	basicService := employeeapp.NewBasicService(emailNotifier)

	// WHEN
	_, err := basicService.Add(ctx, newEmployee)
	// THEN
	if err != nil {
		t.Errorf("no error was expected but got: %q", err)
	}

	assert.Equal(t, expectedMessage, emailNotifier.messages[0])
}
```

For the notifier dependency, we have created emailNotifierMock below.
```go
// emailNotifierMock is a Notifier mock that track
// received messages
type emailNotifierMock struct {
	messages []employeeapp.Message
}

func NewEmailNotifierMock() *emailNotifierMock {
	return &emailNotifierMock{
		messages: make([]employeeapp.Message, 0),
	}
}

// Notify adds the given message to the messages map contained in the mock.
func (e *emailNotifierMock) Notify(ctx context.Context, message employeeapp.Message) error {
	e.messages = append(e.messages, message)
	return nil
}
```

If you run the unit test, the result will be successful.
```sh
go test -timeout 30s github.com/group/project/pkg/employeeapp -run ^TestEmailNotification$

ok  	github.com/group/project/pkg/employeeapp
```

now take a look at the synchronous Add() business function. Please see that `Notify()` call is synchronous.
```go
// Add validates and save a new employee.
func (b *basicService) Add(ctx context.Context, newemployee Employee) (string, error) {
	newemployee.ID = uuid.New().String()
	// do some business logic
	b.notify(ctx, newemployee)
	return newemployee.ID, nil
}

// notify notifies to the employee using the given notifier.
func (b *basicService) notify(ctx context.Context, employee Employee) {
	if b.notifier == nil {
		return
	}
	newMessage := Message{
		Subject: "Hey",
		To:      employee.Email,
		From:    "anybody@somewhere.com",
		Body:    "Good morning there",
	}
	notifierErr := b.notifier.Notify(ctx, newMessage)
	if notifierErr != nil {
		log.Printf("unexpected error: %s sending message %+v to %q ", notifierErr, newMessage, employee.Email)
	}
}
```

We now receive a request from the tech leader requesting to call the `Notify()` function asynchronously.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1t9yizzaw6gze94z7hjv.png)


Then we go to `basicService` logic and create a *goroutine* to call the internal notify function.

```go
// Add validates and save a new employee.
func (b *basicService) Add(ctx context.Context, newemployee Employee) (string, error) {
	newemployee.ID = uuid.New().String()
	// do some business logic
	go b.notify(ctx, newemployee)
	return newemployee.ID, nil
}
```

Now we try to run our unit test, but wait, it does not work anymore.

```sh
go test -timeout 30s github.com/group/project/pkg/employeeapp -run ^TestEmailNotification$

--- FAIL: TestEmailNotification (0.00s)
panic: runtime error: index out of range [0] with length 0 [recovered]
	panic: runtime error: index out of range [0] with length 0
```

and it becomes worst when we add the `-race` flag.

```sh
go test -race -timeout 30s github.com/group/project/pkg/employeeapp -run ^TestEmailNotification$
--- FAIL: TestEmailNotification (0.00s)
==================
WARNING: DATA RACE
Write at 0x00c0000932d0 by goroutine 9:
  github.com/group/project/pkg/employeeapp_test.(*emailNotifierMock).Notify()
```

Well that's the reason of this article.

## Solution

... One way to solve this could be to follow my proposal without changing our implementation.

1. Add a `sync.WaitGroup` and a `sync.Mutex` attributes in the Notifier mock. 

```go
// emailNotifierMock is a domain.Notifier mock that track
// received messages
type emailNotifierMock struct {
	err      error
	messages []employeeapp.Message
	mu       sync.Mutex
	wg       sync.WaitGroup
}
```

2. In the `Notify()` function within notifier mock we add a `defer wg.Done()` statement. Here we have to avoid race conditions on the `messages` slice, because we don't know how many goroutines could try to push items into it. That's the reason of the `sync.Mutex` attribute and the `Lock()` and `Unlock()` calls.

```go
// Notify adds the given message to the messages map contained in the mock.
func (e *emailNotifierMock) Notify(ctx context.Context, message employeeapp.Message) error {
	defer e.wg.Done()
	if e.err != nil {
		return e.err
	}
	e.mu.Lock()
	e.messages = append(e.messages, message)
	e.mu.Unlock()
	return nil
}
```

3. In the unit test before calling the `Add()` function, we add the `emailNotifier.wg.Add(1)` statement and after calling the `Add()` function we add the following also `emailNotifier.wg.Wait()`.

```go
	...
	// WHEN
	emailNotifier.wg.Add(1)
	_, err := basicService.Add(ctx, newEmployee)
	emailNotifier.wg.Wait()
	// THEN
	...
```

obtaining this result.

```go
import (
	"context"
	"sync"
	"testing"

	"github.com/group/project/pkg/employeeapp"
	"github.com/stretchr/testify/assert"
)

func TestEmailNotification(t *testing.T) {
	// GIVEN
	newEmployee := employeeapp.Employee{
		FirstName: "Daenerys",
		LastName:  "Targaryen",
		Email:     "someone@somewhere.com",
		Salary:    25000,
	}
	expectedMessage := employeeapp.Message{
		Subject: "Hey",
		To:      "someone@somewhere.com",
		From:    "anybody@somewhere.com",
		Body:    "Good morning there",
	}
	ctx := context.TODO()
	emailNotifier := NewEmailNotifierMock()
	basicService := employeeapp.NewBasicService(emailNotifier)

	// WHEN
	emailNotifier.wg.Add(1)
	_, err := basicService.Add(ctx, newEmployee)
	emailNotifier.wg.Wait()
	// THEN
	if err != nil {
		t.Errorf("no error was expected but got: %q", err)
	}

	assert.Equal(t, expectedMessage, emailNotifier.messages[0])
}
```

now let's check the unit test again.

```sh
go test -race -timeout 30s github.com/group/project/pkg/employeeapp -run ^TestEmailNotification$
ok      github.com/group/project/pkg/employeeapp
```

## Conclusion

1. Always make your code testeable. In the above scenario, the notifier has a well-known interface and even internally in the service, we are calling a private function.
2. The interfaces go a long way in making the code testable.
3. As much as possible, just test the public or visible features. Try not to make private functions part of your base code contract. That will give you flexibility in the future. Otherwise, feel free to test them with special care.
4. `sync.WaitGroup` library help us to wait for goroutines.
5. Mocks provide the opportunity to have control over the behavior of the code.
6. Remember to use `go test -race` if the unit test involves goroutines.

## Acknowledgement

* For modeling I was using [PlantUML](https://plantuml.com)