How to Write Tests
Test file naming
- Test files must end with
_test.go - Place test files in the same package as the code being tested
- Example:
hash.go→hash_test.go
Test function naming
// Pattern: Test<FunctionName>_<Scenario>
func TestGenerateHashPassword_ValidInput(t *testing.T) { }
func TestLogin_InvalidCredentials(t *testing.T) { }
func TestUser_HasRole_MultipleRoles(t *testing.T) { }
Using test helpers
Database setup
func TestMyFunction(t *testing.T) {
// Setup in-memory SQLite database
db := testhelpers.SetupTestDBWithGlobalDB(t)
// Database is automatically cleaned up after test
// model.DB is set for code that uses the global DB
}
Creating test users
// Create user with role
user := testhelpers.CreateTestUser(t, db, "test@example.com", "Parent")
// Create user with password
user := testhelpers.CreateTestUserWithPassword(t, db, "test@example.com", "password123", "Admin")
Generating JWT tokens
secret := testhelpers.GetTestSecret()
os.Setenv("APP_SECRET", secret)
defer os.Unsetenv("APP_SECRET")
// Generate valid token
token := testhelpers.GenerateTestToken(t, secret, userID, email)
// Generate expired token
expiredToken := testhelpers.GenerateExpiredToken(t, secret, userID, email)
Using testify
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExample(t *testing.T) {
// assert - test continues on failure
assert.Equal(t, expected, actual, "optional message")
assert.NotNil(t, obj)
assert.True(t, condition)
assert.Contains(t, haystack, needle)
// require - test stops on failure (use for prerequisites)
require.NoError(t, err, "this is critical")
require.NotNil(t, user, "user must exist")
}
Table-driven tests
func TestHashPassword_Variants(t *testing.T) {
tests := []struct {
name string
password string
wantErr bool
}{
{
name: "valid password",
password: "SecurePass123!",
wantErr: false,
},
{
name: "empty password",
password: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash, err := GenerateHashPassword(tt.password)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, hash)
})
}
}
Testing Gin handlers
func TestLoginHandler(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// Create request
form := url.Values{}
form.Add("Email", "test@example.com")
form.Add("Password", "password")
req, _ := http.NewRequest(http.MethodPost, "/login",
strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
c.Request = req
// Execute
Login(c)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
}
Conventions
Test independence
- Each test should be independent and self-contained
- Use
t.Cleanup()ordeferfor teardown - Don't rely on test execution order
- Avoid global state when possible
Arrange-Act-Assert pattern
func TestExample(t *testing.T) {
// Arrange - setup test data
user := CreateTestUser(...)
// Act - execute the code under test
result := user.HasRole("Admin")
// Assert - verify the outcome
assert.True(t, result)
}
Test edge cases
Always test:
- Happy path (valid input)
- Invalid input
- Empty/nil values
- Boundary conditions
- Error scenarios
- Domain-specific edge cases
Use require for prerequisites
func TestExample(t *testing.T) {
user, err := CreateUser(...)
require.NoError(t, err) // Stop if creation fails
require.NotNil(t, user) // Stop if user is nil
// Now safe to continue
assert.Equal(t, "test@example.com", user.Email)
}
Writing benchmarks
func BenchmarkGenerateHashPassword(b *testing.B) {
password := "BenchmarkPassword123!"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = GenerateHashPassword(password)
}
}
Best practices
- Write tests first (TDD) or immediately after implementing features
- Keep tests simple — one logical assertion per test when possible
- Use table-driven tests for multiple similar scenarios
- Mock external dependencies (databases, APIs)
- Test error handling as thoroughly as success paths
- Maintain test coverage above 70% for critical code
- Run tests before committing code changes
- Keep tests fast — use in-memory databases, avoid sleeps
- Document complex test scenarios with comments
- Review test coverage reports regularly