In the previous lesson, we learned how to securely store users’ passwords using bcrypt and implemented the createUser API. We didnt implement unit test on this createUser API. Now, it’s time to make our unit tests stronger.
When testing APIs with gomock, we often use matchers like gomock.Any() or gomock.Eq().This tutorial demonstrates how to write robust unit tests for APIs that involve password hashing using custom GoMock matchers.
When testing an API endpoint that creates a user, we need to verify that the CreateUser function is called with the correct parameters. However, since the user’s password is hashed before being stored, a simple equality check won’t work.
Table of Contents
The Testing Challenge
Why Standard Matchers Fail with Password Hashing
When testing APIs that hash passwords, we encounter a unique problem. Specifically, we’ll tackle the challenge of testing code that involves password hashing with bcrypt, which produces a different hash for the same input every time.
The Challenge:
- bcrypt uses random salt for each hash operation
- Same password produces different hashes every time
- Standard
gomock.Eq()matcher expects exact equality - Tests become unreliable or too weak
// These will NEVER be equal due to random salt
hash1 := bcrypt.GenerateFromPassword([]byte("password123"), 10)
hash2 := bcrypt.GenerateFromPassword([]byte("password123"), 10)
// hash1 != hash2 (even though same password)
In previous tutorial api/user_test.go in TestCreateUserAPI, we first create a randomUser function to be create random user. 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.
func TestCreateUserAPI(t *testing.T) {
user, password := randomUser(t)
testCases := []struct {
name string
body gin.H
buildStubs func(store *mockdb.MockStore)
checkResponse func(recorder *httptest.ResponseRecorder)
... test case interation
There are several different cases we can test check previous tutorial, we iterate through all these cases and run a separate sub-test for each of them and in each sub-test we create a new gomock controller and use it to build a new mock DB store. We then call the buildStubs() function of the test case to setup 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.
Now in today lecture we only focus the test case 1 that is the successful case, here we faces some issues related to the Unit testing. When testing APIs with gomock, we often use matchers like gomock.Any() or gomock.Eq(). However, for cases involving hashed passwords, these matchers fall short because bcrypt produces different hashes each time (due to random salt).
Let”’s explore this problem and its solution step-by-step.
Part 1: The Initial (Weak) Test with gomock.Any()in api/user_test.go
In the previous lesson, we implemented a CreateUser API that hashes passwords using bcrypt. When writing a unit test for this handler, a common but flawed approach is to use gomock.Any() for the database call arguments. The problem with this weak test due to non-deterministric hashing. The gomock.Any() matcher in CreateUser matches any input value. It does not validate the data being passed to your database layer and this creates a weak test.

Why is this bad?
This test is too weak. It can’t detect critical bugs. For example:
- Discarding all parameters: If the handler accidentally passes an empty
CreateUserParamsobject to the store, the test will still pass! - Incorrect password hashing: If the handler hashes a static string instead of the user’s actual password, the test will also pass!
In both scenarios, the test gives us a false sense of security. This test would pass even with broken implementations:
In this argument we removed all the CreateUserParams object fields. Setting empty object to db.CreateUserParams will discard all the inputs parameter of the request and we create completly empty user in database but we still pass the test case. This is because the implementation of the handler is completely wrong but test could not detect it, case 1: empty input object.

case 2 : Let’s remove the req.Password in util.HashPassword and instead we just hash a constant value, such as “xyz” here. Then go back to the test, and run it again. As you can see, it still passed! This is unacceptable and test we wrote is too weak!

Now what is solution for above issues, we can use something else, such as the gomock.Eq() matcher?
Part 2: Attempting a Fix with gomock.Eq()
The solution for above issues, You might think of replacing gomock.Any() with gomock.Eq(): The intuitive fix is to use the equality matcher, gomock.Eq(expectedArgument) . This matcher checks if the provided argument exactly matches the expected value.
In api/user_test/go, first to declare a new argument variable of tpe db.CreateUserParams and HashedPassword field is generate via util.HashPassword.
func TestCreateUser_WithEqMatcher(t *testing.T) {
user := randomUser()
password := util.RandomString(6)
// This will fail because hashes are different each time
hashedPassword, err := util.HashPassword(password)
require.NoError(t, err)
buildStubs: func(store *mockdb.MockStore) {
arg := db.CreateUserParams{
Username: user.Username,
HashedPassword: hashedPassword, // Hash created in test
FullName: user.FullName,
Email: user.Email,
}
// ❌ This will ALWAYS fail due to different hash values
store.EXPECT().
CreateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(user, nil)
}
}
Why the gomock.Eq() Doesn’t Work
- Test creates hash:
$2a$10$ABC123... - Create user Handler creates hash:
$2a$10$XYZ789... - Same password, different hashes →
gomock.Eq()fails
This test will consistently fail. bcrypt generates a unique hash each time it’s run (by using a random salt). Therefore, the hashedPassword generated in the test will never be equal to the hashedPassword generated inside the API handler, even though the original password is the same.
The test fails with a “missing call” error because the arguments don’t match.So we cannot simply use the built-in gomock.Eq() matcher to compare the argument. Only way to fixed the above issued related to unit test case 1 success of hashed pasword related is to implment a new custom matcher.
Part 3: The Solution – A Custom gomock Matcher
The correct way to solve this is to implement our own custom gomock matcher. A custom matcher allows you to define the logic for how two objects should be compared. In our case, we need to compare all fields normally except for the hashed password, which we need to verify using the CheckPassword function.
Step 1: Understanding the Matcher Interface
// GoMock Matcher interface
type Matcher interface {
Matches(x interface{}) bool
String() string
}
A gomock.Matcher is an interface with two methods:
Matches(x interface{}) bool: This is the core of the matcher. It returnstrueif the inputxis a match.String() string: This returns a description of the matcher, which is used for logging when tests fail.- For our custom matcher, we’ll have to write a similar implementation of the matcher interface, which has only 2 methods.
Built-in Matcher Examples : for more information, the custom matcher we are going to implement wil be very similary to one with green background.
// Any matcher - always returns true
type anyMatcher struct{}
func (anyMatcher) Matches(x interface{}) bool {
return true
}
func (anyMatcher) String() string {
return "is anything"
}
// Eq matcher - compares for equality
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)
}
Step 2: Implementing Our Custom Matcher
We’ll create a matcher that compares all fields of the CreateUserParams struct, but for the HashedPassword, it will use our application’s CheckPassword utility to verify it against the original plain-text password.
We need a matcher that:
- Accepts the plain password.
- Uses
util.CheckPassword()to validate the stored hash. - Compares all other fields with
reflect.DeepEqual.
Let’s add this code at top of our existing api/user_test.go file.
package api
import (
"bytes"
"database/sql"
...)
// Custom matcher for CreateUserParams with password verification
type eqCreateUserParamsMatcher struct {
arg db.CreateUserParams
password string // The plaintext password to check against
}
// Matches implements the gomock.Matcher interface
func (e eqCreateUserParamsMatcher) Matches(x interface{}) bool {
// Convert interface to expected type
arg, ok := x.(db.CreateUserParams)
if !ok {
return false
}
// Check if the hashed password matches the expected plain password
err := util.CheckPassword(e.password, arg.HashedPassword)
if err != nil {
return false
}
// Set the hashed password to the same value for deep comparison
e.arg.HashedPassword = arg.HashedPassword
// Compare all other fields using deep equality
return reflect.DeepEqual(e.arg, arg)
}
// String implements the gomock.Matcher interface for logging
func (e eqCreateUserParamsMatcher) String() string {
return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password)
}
func TestCreateUserAPI(t *testing.T){
Explanation:
- The
Matchesfunction first checks if the received argumentxis of typedb.CreateUserParams. - It then uses
util.CheckPasswordto verify that the plain-text password (e.password) matches the hashed password from the handler (arg.HashedPassword). - If the password check is successful, we copy the generated hash into our expected argument (
e.arg.HashedPassword = arg.HashedPassword). This is a crucial step! - Finally, we use
reflect.DeepEqualto ensure that all other fields in the struct are identical.
Step 3: Creating a Constructor Function
Next we’ll add a function to return an instance of our custom matcher from step 2. For convenience, we create a function that returns an instance of our new matcher. This is similar to how gomock.Eq() or gomock.Any() are used add this code below our step 2 code and before func TestCreateUserAPI(t *testing) {.
The EqCreateUserParams is a helper function to create a custom matcher and provides a clean API for use in your tests, similar to gomock.Eq().
// Constructor function for the custom matcher
func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher {
return eqCreateUserParamsMatcher{arg, password}
}
Step 4:Integrating the Custom Matcher into Your Test
Now, replace the weak gomock.Any() and the failing gomock.Eq() in your test case with your new, powerful custom matcher.
Before (Weak):
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()). // Doesn't check anything!
Times(1).
Return(user, nil)
Now, we can update our test to use our powerful new matcher. After strong and corrected. In our api/.go we need to change the gomock.Eq to EqCreateUserParams(arg, user.Password))
// Build the expected argument object (note: the HashedPassword can be empty/dummy, it gets overwritten)
arg := db.CreateUserParams{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
// HashedPassword: ... we don't need to precompute it anymore!
}
store.EXPECT().
CreateUser(gomock.Any(), EqCreateUserParams(arg, user.Password)). // Use custom matcher
Times(1).
Return(user, nil)
Notice we no longer need to pre-hash the password in the test. We pass the expected CreateUserParams (without the hashed password) and the original plain-text password to our matcher.
Verification
With this new matcher:
- The test passes when the handler code is correct.
- The test fails if we try to pass empty params arg=db.CreateUserParams{}.
- The test fails if we hash the wrong password in the handler.
Our unit test is now much stronger and can correctly detect bugs in the CreateUser handler.
Conclusion
Implementing a custom gomock matcher is a powerful technique for handling complex testing scenarios. While it requires a bit of extra code, it gives you full control over the comparison logic, leading to more robust and reliable tests.