How to write stronger unit tests with a custom go-mock matcher
Tags: go, tutorial, testing, beginners
Published: 2021-02-21 12:58:49

Hello everyone!

In this lecture, we will learn how to write a custom gomock matcher to make our Golang unit tests stronger.

{% youtube DuzBE0jKOgE %}

Here's:

- Link to the [full series playlist on Youtube](https://bit.ly/backendmaster)
- And its [Github repository](https://github.com/techschool/simplebank)

## The weak unit test

In the last lecture, we have learned how to securely store users’ password using bcrypt. We have also implemented the API to create a new user for our simple bank.

```go
func (server *Server) createUser(ctx *gin.Context) {
    var req createUserRequest
    if err := ctx.ShouldBindJSON(&req); err != nil {
        ctx.JSON(http.StatusBadRequest, errorResponse(err))
        return
    }

    hashedPassword, err := util.HashPassword(req.Password)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    arg := db.CreateUserParams{
        Username:       req.Username,
        HashedPassword: hashedPassword,
        FullName:       req.FullName,
        Email:          req.Email,
    }

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    rsp := createUserResponse{
        Username:          user.Username,
        FullName:          user.FullName,
        Email:             user.Email,
        PasswordChangedAt: user.PasswordChangedAt,
        CreatedAt:         user.CreatedAt,
    }
    ctx.JSON(http.StatusOK, rsp)
}
```

Although I didn’t show you how to write unit tests for this API, since it would be very similar to what we have done in lecture 13 of the course. With the help of go-mock, any API unit tests can be written with ease.

However, if you have tried to write unit tests for the create user API yourself, you might find it a little bit tricky, due to the fact that the input password param is hashed before storing in the database.

To understand why, let’s look at the simple version of the tests that I’ve already written here.

```go
func TestCreateUserAPI(t *testing.T) {
    user, password := randomUser(t)

    testCases := []struct {
        name          string
        body          gin.H
        buildStubs    func(store *mockdb.MockStore)
        checkResponse func(recoder *httptest.ResponseRecorder)
    }{
        {
            name: "OK",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(1).
                    Return(user, nil)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusOK, recorder.Code)
                requireBodyMatchUser(t, recorder.Body, user)
            },
        },
        {
            name: "InternalError",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(1).
                    Return(db.User{}, sql.ErrConnDone)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusInternalServerError, recorder.Code)
            },
        },
        {
            name: "DuplicateUsername",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(1).
                    Return(db.User{}, &pq.Error{Code: "23505"})
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusForbidden, recorder.Code)
            },
        },
        {
            name: "InvalidUsername",
            body: gin.H{
                "username":  "invalid-user#1",
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(0)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusBadRequest, recorder.Code)
            },
        },
        {
            name: "InvalidEmail",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     "invalid-email",
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(0)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusBadRequest, recorder.Code)
            },
        },
        {
            name: "TooShortPassword",
            body: gin.H{
                "username":  user.Username,
                "password":  "123",
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(0)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusBadRequest, recorder.Code)
            },
        },
    }

    ...
}
```

Basically, we first create a random user to be created.

Then we declare a table of test cases, where we can define the input request body, and 2 functions to build store stubs and check the response of the API.

There are several different cases we can test, such as:

- The successful case
- Internal server error case
- Duplicate username case
- Invalid username, email, or password case

We iterate through all of these cases, and run a separate sub-test for each of them.

```go
func TestCreateUserAPI(t *testing.T) {
    ...

    for i := range testCases {
        tc := testCases[i]

        t.Run(tc.name, func(t *testing.T) {
            ctrl := gomock.NewController(t)
            defer ctrl.Finish()

            store := mockdb.NewMockStore(ctrl)
            tc.buildStubs(store)

            server := NewServer(store)
            recorder := httptest.NewRecorder()

            // Marshal body data to JSON
            data, err := json.Marshal(tc.body)
            require.NoError(t, err)

            url := "/users"
            request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
            require.NoError(t, err)

            server.router.ServeHTTP(recorder, request)
            tc.checkResponse(recorder)
        })
    }
}
```

In each sub-test, we create a new gomock controller, and use it to build a new mock DB store.

Then we call the `buildStubs()` function of the test case to set up the stubs for that store.

After that, we create a new server using the mock store, and create a new HTTP response `recorder` to record the result of the API call.

Next we marshal the input request body to JSON, and make a new POST `request` to the create-user API endpoint with that JSON data.

We call `server.router.ServeHTTP()` function with the `recorder` and `request` object. And finally just call `tc.checkResponse()` function to check the result.

It’s pretty simple, just like what we’ve learned in [Lecture 13](https://dev.to/techschoolguru/mock-db-for-testing-http-api-in-go-and-achieve-100-coverage-4pa9).

I highly recommend you to read it first to make sure you fully understand the code before continue.

Now, for today’s lecture, we only need to focus on 1 case: the successful one.

```go
func TestCreateUserAPI(t *testing.T) {
    user, password := randomUser(t)

    testCases := []struct {
        name          string
        body          gin.H
        buildStubs    func(store *mockdb.MockStore)
        checkResponse func(recoder *httptest.ResponseRecorder)
    }{
        {
            name: "OK",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Any()).
                    Times(1).
                    Return(user, nil)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusOK, recorder.Code)
                requireBodyMatchUser(t, recorder.Body, user)
            },
        },
        ...
    }

    ...
}
```

As you can see here, the request body’s parameters are all valid.

In the build stubs function, we expect the `CreateUser()` function of the store to be called with 2 parameters. In this simple version, we’re using the `gomock.Any()` matcher for both of them.

Note that the first argument of the `store.CreateUser()` function is a context, which we don’t care about its value, so it makes sense to use any matcher.

However, using that same matcher for the second argument will weaken the test, because it won’t be able to detect if the `createUserParams` object passed in the `CreateUser()` function is correct or not. I’m gonna show you how in a moment. For now, let’s just keep this `gomock.Any()` matcher.

And this `CreateUser` function is expected to be called exactly once, and it will return the user object with no errors, since this is the happy case.

For the check response function, we just check the HTTP status code to be `200 OK`, and make sure that the response body matches the created user object.

That’s it! Let’s run the test.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r96gxjyw6hs5bpag5ylg.png)

All passed. So we’re good, right?

Well, not really!

As I said before, this `gomock.Any()` matcher will make the test weaker.

How?

Let’s see what will happen if in this `createUser()` handler, I set the argument variable to an empty `CreateUserParam{}` object.

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    arg := db.CreateUserParams{}

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

This will discard all the input parameters of the request, and will create a completely empty user in the database. So it should not be allowed, and we expect the test to fail, right?

However, if we run the test again:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p538rdyl88x6ip6haluz.png)

It still passed!

This is very bad, because the implementation of the handler is completely wrong, but the test could not detect it!

Another case that this test would not be able to detect is like this:

Let’s remove this set empty argument statement, but I will ignore the user’s input password, and just hash a constant value, such as "xyz" here.

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    hashedPassword, err := util.HashPassword("xyz")
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    arg := db.CreateUserParams{
        Username:       req.Username,
        HashedPassword: hashedPassword,
        FullName:       req.FullName,
        Email:          req.Email,
    }

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

Then go back to the test, and run it again.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/291bhky0fwfp24xwf0gd.png)

As you can see, it still passed!

This is unacceptable! The test we wrote is too weak! We need to fix it!

## Try using gomock.Eq

One thing that might come to your mind is: what if instead of using `gomock.Any()` matcher, we use something else, such as the `gomock.Eq()` matcher? Let’s try it!

First I will declare a new `arg` variable of type `db.CreateUserParams`, where `username` is `user.Username`.

For the `HashedPassword` field, we need to hash the input naked password, so let’s go up to the top of the test. Here, after generating the random user object, we call `util.HashPassword`, and pass in the generated password value.

This function will return a `hashedPassword` value and an error, so we have to make sure error is `nil` using `require.NoError()`.

```go
func TestCreateUserAPI(t *testing.T) {
    user, password := randomUser(t)

    hashedPassword, err := util.HashPassword(password)
    require.NoError(t, err)

    testCases := []struct {
        name          string
        body          gin.H
        buildStubs    func(store *mockdb.MockStore)
        checkResponse func(recoder *httptest.ResponseRecorder)
    }{
        {
            name: "OK",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                arg := db.CreateUserParams{
                    Username: user.Username,
                    HashedPassword: hashedPassword,
                    FullName: user.FullName,
                    Email: user.Email,
                }
                store.EXPECT().
                    CreateUser(gomock.Any(), gomock.Eq(arg)).
                    Times(1).
                    Return(user, nil)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusOK, recorder.Code)
                requireBodyMatchUser(t, recorder.Body, user)
            },
        },
        ...
    }

    ...
}
```

Next, the `FullName` should be `user.FullName`, and finally `Email` should be `user.Email`.

With this object, we can now replace `gomock.Any()` matcher with `gomock.Eq(arg)`

OK now, let’s try to test the case where all input parameters are discard.

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    arg := db.CreateUserParams{}

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

Since we have used a stronger `Eq()` matcher here, the test should fail, right? Let’s run it to confirm!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gurmh07q4p4dqi955hr8.png)

Yes, that’s right! The test failed as expected.

The logs tell us it failed because of the missing calls, which is true, because although the `CreateUser` function was called, it was not called with the correct input argument as we wanted.

So is that it? Did we fixed the issue?

Unfortunately no! Let’s see what will happen if I remove this line `arg := db.CreateUserParams{}`

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    arg := db.CreateUserParams{
        Username:       req.Username,
        HashedPassword: hashedPassword,
        FullName:       req.FullName,
        Email:          req.Email,
    }

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

Now the create user handler function is correctly taken into account all input parameters, so we expect the test to pass, right? Let’s run it to confirm!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/599q2rfmx148wzx6jylx.png)

Sadly, the test didn’t pass. It still failed due to missing call.

And the real reason is that the `CreateUser()` function is called with an input argument that doesn’t match with the one we expect.

In the log, we can see clearly what value the mock store got, compared to what it wants to receive.

It looks like the `Username`, `FullName`, and `Email` are all matched. Only the `HashedPassword` values are different. Do you know why?

Well, if you still remember what we learned in [the last lecture about bcrypt](https://dev.to/techschoolguru/how-to-securely-store-passwords-3cg7), it uses a random salt when hashing the password to prevent rainbow table attack.

So even if we pass the same password value into the hash function, it will always produce a new hashed value every time.

Because of this, the hashed value that we created in the test and the one in the create user handler will always be different. So we cannot simply use the built-in `gomock.Eq()` matcher to compare the argument.

The only way to fix this properly is to implement a new custom matcher on our own in this case. Although it sounds a bit annoying, it’s actually very easy to implement. And I think it would be useful for you if you ever encounter some special cases like this in your real project.

OK, let’s learn how to do it!

## Implement a custom gomock matcher

First I will remove the `hashedPassword` because it is not needed in the custom matcher that we’re going to implement.

We will have to replace the `gomock.Eq` matcher with our own matcher. So let’s open its implementation.

```go
func Eq(x interface{}) Matcher { return eqMatcher{x} }

type Matcher interface {
    // Matches returns whether x is a match.
    Matches(x interface{}) bool

    // String describes what the matcher matches.
    String() string
}
```

It’s simply a function that takes the expected argument `x` as input and returns a `Matcher` interface. In this specific case, it returns an implementation of the matcher that matches on equality: `eqMatcher`.

For our custom matcher, we will have to write a similar implementation of the `Matcher` interface, which has only 2 methods:

- The first one is `Matches()`, which should return whether the input `x` is a match or not.
- And the second one is `String()`, which just describes what the matcher matches for logging purpose.

There are several built-in implementations of the `Matcher` interface. For example, this one is `anyMatcher`, which will always return true regardless of the input argument.

```go
type anyMatcher struct{}

func (anyMatcher) Matches(interface{}) bool {
    return true
}

func (anyMatcher) String() string {
    return "is anything"
}
```

Then this is the equal matcher that we’re using:

```go
type eqMatcher struct {
    x interface{}
}

func (e eqMatcher) Matches(x interface{}) bool {
    return reflect.DeepEqual(e.x, x)
}

func (e eqMatcher) String() string {
    return fmt.Sprintf("is equal to %v", e.x)
}
```

It uses `reflect.DeepEqual` to compare the actual input argument with the expected one.

The custom matcher that we’re going to implement will be very similar to this one, so I’m gonna copy all of these functions, and paste them to the top of the `user_test.go` file.

Then let’s change the name of this struct to `eqCreateUserParamsMatcher`. In order to compare the input arguments correctly, we will need to store 2 fields this struct:

- First the `arg` field of type `db.CreateUserParams`
- And second, the `password` field to store the naked password value.

```go
type eqCreateUserParamsMatcher struct {
    arg      db.CreateUserParams
    password string
}
```

OK, now let’s implement the `Matches()` function. Since the input parameter `x` is an interface, we should convert it to `db.CreateUserParams` object.

If the conversion is not OK, then we just return false. Otherwise, we will check if the hashed password matches with the expected password or not by calling `util.CheckPassword()` function with `e.password` and `arg.HashedPassword`.

```go
func (e eqCreateUserParamsMatcher) Matches(x interface{}) bool {
    arg, ok := x.(db.CreateUserParams)
    if !ok {
        return false
    }

    err := util.CheckPassword(e.password, arg.HashedPassword)
    if err != nil {
        return false
    }

    e.arg.HashedPassword = arg.HashedPassword
    return reflect.DeepEqual(e.arg, arg)
}
```

If this function returns an error, then we return `false`. Else, we will set the `hashedPassword` field of the expected argument `e.arg` to the same value with the input `arg.HashPassword`.

And we use `reflect.DeepEqual` to compare the expected argument `e.arg` with the input argument `arg`.

That’s all! Pretty simple, right?

OK, now let’s update this message of the String function to include the expected argument and naked password values.

```go
func (e eqCreateUserParamsMatcher) String() string {
    return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password)
}
```

And our custom matcher is done.

Next we will add a function to return an instance of this matcher, just like the `Eq()` function that returns an `EqMatcher` instance.

I’m gonna change this function’s name to `EqCreateUserParams()`, and it will have 2 input arguments: a `db.CreateUserParams` object, and a naked `password` string.

This function will return a `Matcher` interface, which in our case is the `eqCreateUserParamsMatcher` object with the input argument and password.

```go
func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher {
    return eqCreateUserParamsMatcher{arg, password}
}
```

Alright, so now we have everything we need for the new custom matcher. Let’s use it in the unit test to see how it goes.

I’m gonna change the `gomock.Eq()` to `EqCreateUserParams()`, then pass in the `CreateUserParams` argument `arg` and naked `password`.

```go
func TestCreateUserAPI(t *testing.T) {
    user, password := randomUser(t)

    testCases := []struct {
        name          string
        body          gin.H
        buildStubs    func(store *mockdb.MockStore)
        checkResponse func(recoder *httptest.ResponseRecorder)
    }{
        {
            name: "OK",
            body: gin.H{
                "username":  user.Username,
                "password":  password,
                "full_name": user.FullName,
                "email":     user.Email,
            },
            buildStubs: func(store *mockdb.MockStore) {
                arg := db.CreateUserParams{
                    Username: user.Username,
                    FullName: user.FullName,
                    Email:    user.Email,
                }
                store.EXPECT().
                    CreateUser(gomock.Any(), EqCreateUserParams(arg, password)).
                    Times(1).
                    Return(user, nil)
            },
            checkResponse: func(recorder *httptest.ResponseRecorder) {
                require.Equal(t, http.StatusOK, recorder.Code)
                requireBodyMatchUser(t, recorder.Body, user)
            },
        },
        ...
    }

    ...
}
```

Just like that, and we’re done. Let’s rerun the test!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lle8cmf2lysanh0ab7lb.png)

It passed. Excellent!

Now let’s try the case where we set this argument to an empty `db.CreateUserParams{}` object.

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    arg := db.CreateUserParams{}

    user, err := server.store.CreateUser(ctx, arg)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

We expect the test to fail.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s6kfrryk8ztphfh8xein.png)

And it does fail, since the input argument doesn’t match the expected one.

OK, how about the case where we ignore the input password parameter, and just hash this constant password value: `xyz`

```go
func (server *Server) createUser(ctx *gin.Context) {
    ...

    hashedPassword, err := util.HashPassword("xyz")
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ...
}
```

Let’s run the test!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icx7l4d0459u3ja2rt8r.png)

It failed, just like what we expected.

So now our unit test has become much stronger than it was before. Thanks to the new custom matcher that we’ve just implemented.

And that’s it for today’s lecture. I hope you find it interesting and useful.

Thanks for reading, and see you in the next one!

---

If you like the article, please subscribe to our [Youtube channel](http://bit.ly/techschoolguru) and follow us on [Twitter](https://twitter.com/techschoolguru) or [Facebook](https://www.facebook.com/techschool.guru) for more tutorials in the future.

---
If you want to join me on my current amazing team at Voodoo, check out [our job openings here](https://bit.ly/voodoojobs). Remote or onsite in Paris/Amsterdam/London/Berlin/Barcelona with visa sponsorship.