Don't Let Your Mocks Mock You!

Mock testing is useful, but there is a risk of building false confidence when we don't pay close attention.

Here’s a short one about a pattern that we can unfortunately too often observe when it comes to mocks. Especially with databases, we see our “Repository” or “Database Connection” mocked out in tests. That is, for the sake of unhappy path testing. However, more often than not we also see that these mocks are used for happy path testing.

There is a real danger where our mocks are mocking us.

The Bad

Let’s look at some very simple code. We have a service that gets a request and ends up inserting data into a DB, via some repository. Potentially, the code is close to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// service.go
func (s *Service) InsertIntoDb(request Request) (Response, error) {
    dbQuery, err := buildInsertQuery(request)
    // if err ! nil ...

    // The actual insert
    result, err := s.repository.
        ExecuteQuery(
            dbQuery.TableName,
            dbQuery.Query,
        );
    // if err != nil ...

    return result, nil
}
1
2
3
4
5
6
7
8
9
// repository.go
func (r *Repository) ExecuteQuery(
    query string, 
    tableName string
) (Response, error) {
    // For the sake of the argument
    // We take 2 strings as input
    // ... don't do that at home!
}

The mistake may be obvious to you now as a reader, as the code is very small and colocated. In reality, it is hard to spot that the first parameter is expected to be the query string, and the second parameter to be the table name. Especially, if the reviewer of this code does not look at the signature of ExecuteQuery (and ask yourself, how often do you open a library and check that the call you see in a PR is correct?).

The Ugly

Consider a test like below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func Test_InsertIntoDb(t *testing.T) {
    mockRepo := new(MockRepository)
    service := &Service{repository: mockRepo}

    req := Request{TableName: "users", Data: "foo"}
    expectedResp := Response{OK: true}

    mockRepo.
        On("ExecuteQuery", "users", "INSERT {...}").
        Return(expectedResp, nil).
        Once()

    resp, err := service.InsertIntoDb(req)
    // NoError, Resp is OK
    mockRepo.AssertExpectations(t)
}

While the test looks right on the surface, are we actually testing that our insert works? Nope. At best, we verify that some string was passed as the first parameter, and some other string was passed as the second one, supposedly the table name and the insert query respectively. But looking at the method’s signature, the order would be to first pass the query and then pass the table name.

Oh no, anyway!, with this test (and all other mock tests), we hit 100% Code Coverage! Great, our system is fully tested, straight to production!

The Good

If we want to verify our happy path with integrated technologies, we can oftentimes use the real deal in integration tests (and if we can’t, we should really work towards being able to). Pretty much any major technology, from cloud provider over databases to message brokers, is offered as a testcontainer. Using Testcontainers for Integration Tests, or connecting to a real instance and creating ephemeral data, can greatly increase our confidence in the unit under test.

On a side note: With Cursor or Claude Code, we’re seeing a lot of seemingly correct mock tests. AI code generators love Mocks, and they often enough end up creating tests that claim to test something way beyond capability. Be sure to pay extra attention there.