DEV Community

Cover image for Unit Tests & Mocking: the Bread and the Butter
Ivan Pesenti
Ivan Pesenti

Posted on

Unit Tests & Mocking: the Bread and the Butter

Premise

Welcome back, folks 🤝 After a while, I finally took the time to start this new series of blog posts around computer science, programming, and Go. This series will be focused on different things we can deal with when dealing with our daily work.

How you should read it

Before getting into it, let me share how this blog post (and the upcoming ones) is meant to be read. This blog post targets a specific subject 📍. Therefore, I suggest you read other resources to gain a broader overview. It aims to be your starting point to further dig into the topic. For sure, I'll manage to share resources whenever necessary. Finally, there won't be any GitHub repositories since the code will be pretty focused and not part of a project. Now, you're ready to embark on the journey with unit testing and Go.

Unit Tests & Mocking

The goal is to be able to write a unit test for a struct that holds a dependency toward another one. Unit testing is a technique in which you test a specific Unit by mocking its dependencies. Let's use an example to better depict it. We're going to write a test for the billing package. This could be one of the several packages in your codebase. For the sake of the demo, everything has been written to the billing.go file (do not do this in production). Within this file, we defined these types:

  • The Invoice struct model, holding the CreatedAt and Amount fields
  • The Archiver interface represents the dependency 💉 our UUT relies on
  • The InvoiceManager struct is our Unit Under Test model
  • The Store struct implementing the Archiver interface

Before showing the code, let me share this drawing to let you understand better the actors:

billing package types

The complete source code looks like this:

package billing

import (
 "errors"
 "fmt"
 "os"
 "time"

 "github.com/google/uuid"
)

type Invoice struct {
 CreatedAt time.Time
 Amount    float64
}

type InvoiceManager struct {
 Archiver Archiver
}

func (i *InvoiceManager) RecordInvoice(invoice Invoice) (err error) {
 id, err := i.Archiver.Archive(invoice)
 if err != nil {
  return err
 }
 fmt.Fprintf(os.Stdout, "recorded invoice with id: %s\n", id)
 return nil
}

type Archiver interface {
 Archive(invoice Invoice) (id string, err error)
}

type Store struct{}

func (s *Store) Archive(invoice Invoice) (id string, err error) {
 if invoice.Amount < 0 {
  return "", errors.New("amount cannot be less than 0")
 }
 // logic omitted for brevity
 return uuid.NewString(), nil
}
Enter fullscreen mode Exit fullscreen mode

The Unit Under Test 🤠

With the previous code in mind, our task is to write a test for the method RecordInvoice based on the InvoiceManager receiver type. The function can take two paths:

  1. The happy path is when the i.Archiver.Archive invocation doesn't return any error
  2. The "unhappy" path is when it gives back an error

As good developers, we are asked to write two unit tests. However, we'll write only the happy path test since we would like to focus more on the steps to get there. After all, everybody knows how to write unit tests for this small piece of code.

First things first: mocking

The InvoiceManager struct depends on the Archiver interface. Let's see how we can mock it. Here, we have two options: hand-writing it or automatically generating it. We opt for the latter even if the project's size is trivial.

Be aware that, in real-life projects, this method can save you a consistent amount of time.

Mockery tool

We'll take advantage of this tool, which can be downloaded here. Before proceeding, make sure you have correctly installed it on your machine. To confirm it, you can run this command in your terminal:

mockery --version
Enter fullscreen mode Exit fullscreen mode

In your terminal, navigate to the root folder of your project and run the following command:

mockery --dir billing/ --name Archiver --filename archiver.go
Enter fullscreen mode Exit fullscreen mode

This command specifies three parameters:

  • --dir is the directory where to look for interfaces to mock: our code is contained within the billing folder
  • --name is the name of the interface to mock: Archiver is the identifier for our interface
  • --filename is the filename for the mock file: we used archiver.go to keep the naming convention consistent

When you run the command, you'll notice a new mock folder has been created. You'll find the archiver.go mock file inside it. By default, the mockery tool creates a new folder (and a new package called mock) to hold all the mocks.

If you're not good with this approach, you can override it when running the tool.

Based on my experience, I think the default behavior might be applied in almost all cases. You might also notice that the compiler started to complain. This is due to missing packages in your project. The fix is easy. You should just run the command go mod tidy where your go.mod file is located. Then, double-check the errors have gone away, and you'll be ready to use the mocks.

The test code

Let's see how we can exploit the scaffolded mock in our unit tests. By the books, a unit test should have three stages: Arrange, Act, and Assert (aka AAA paradigm). We'll cover each of these in the subsequent sections. First, I'm going to show you the code, and then I'll walk you through all the relevant parts of it:

package billing_test

import (
 "testing"
 "time"

 "github.com/ossan-dev/unittestmock/billing"
 "github.com/ossan-dev/unittestmock/mocks"
 "github.com/stretchr/testify/assert"
)

func TestRecordInvoice(t *testing.T) {
 // Arrange
 invoice := billing.Invoice{CreatedAt: time.Now(), Amount: 66.50}
 store := mocks.NewArchiver(t)
 store.On("Archive", invoice).Return("16668b88-34a0-4a25-b1da-6a1875072802", nil).Once()
 uut := &billing.InvoiceManager{
 Archiver: store,
 }
 // Act
 err := uut.RecordInvoice(invoice)
 // Assert
 assert.NoError(t, err)
 store.AssertExpectations(t)
}
Enter fullscreen mode Exit fullscreen mode

Arrange 🧱

Here, we've to invoke the function NewArchiver from the mocks package to get a new instance of our mock. Then, we set it up by using three methods:

  1. The On method specifies to which invocation this mock has to reply (also with what arguments)
  2. The Return method specifies which values to return from the mock when invoked
  3. The Once specifies how many times to return values from this mock

Lastly, we instantiate our UUT by passing in the store mock as the Archiver interface. We can safely proceed.

Act 👊

Within this stage, we invoke the method RecordInvoice defined on the uut variable. No further explanations are needed here.

Assert 🤞

In this final stage, we have to check two things:

  1. The uut variable gives back whatever we expect. In this case, we expect a nil error
  2. The store mock behaves as expected

The second point means that the method Archive has been invoked with the expected arguments, the correct number of times, and so on.

Run Test

Now, we can safely run our test. To run it, we invoke the command:

go test -v ./billing
Enter fullscreen mode Exit fullscreen mode

And that's the outcome on my machine:

=== RUN   TestRecordInvoice
recorded invoice with id: 16668b88-34a0-4a25-b1da-6a1875072802
--- PASS: TestRecordInvoice (0.00s)
PASS
ok      github.com/ossan-dev/unittestmock/billing       0.004s
Enter fullscreen mode Exit fullscreen mode

That's a Wrap

I hope you found this blog post helpful. As you may imagine, there are several things to cover regarding unit testing, mocking, etc. Any feedback is highly appreciated.

Before leaving, I strongly invite you to reach out if you are interested in some topics and want me to address them. I swear I shortlist them and do my best to deliver helpful content.

Thank you very much for the support, and see you in the next one 👋

Top comments (0)