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 theCreatedAt
andAmount
fields - The
Archiver
interface represents the dependency π our UUT relies on - The
InvoiceManager
struct is our Unit Under Test model - The
Store
struct implementing theArchiver
interface
Before showing the code, let me share this drawing to let you understand better the actors:
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
}
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:
- The happy path is when the
i.Archiver.Archive
invocation doesn't return any error - 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
In your terminal, navigate to the root folder of your project and run the following command:
mockery --dir billing/ --name Archiver --filename archiver.go
This command specifies three parameters:
-
--dir
is the directory where to look for interfaces to mock: our code is contained within thebilling
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 usedarchiver.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)
}
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:
- The
On
method specifies to which invocation this mock has to reply (also with what arguments) - The
Return
method specifies which values to return from the mock when invoked - 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:
- The
uut
variable gives back whatever we expect. In this case, we expect anil
error - 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
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
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)