# Testing Best Practices
## Core Principles
### Follow TDD
- Iterate with small chunks of logic/code
- Add stub implementation first if needed
- Write test to cover new logic or new behavior
- Run test to see if it fails
- Implement the minimal amount of code to make the test pass
- Repeat the process until your code is complete
### Test What Matters
- **Focus on business logic** - Test the core functionality your code needs to provide
- **Avoid excessive tests** - Don't test scenarios that aren't relevant to your actual use cases
- **Test behavior, not implementation** - Focus on what the code does, not how it does it
### Keep It Simple
- **Pragmatic mocks** - Use simple mock implementations, avoid over-engineering
- **Minimal test setup** - Only setup what's necessary for the specific test case
- **Clear test names** - Use descriptive names that explain what behavior is being tested
### Don't Test the Framework
- **Skip infrastructure testing** - Don't test that logging works, that HTTP requests work, etc.
- **Trust the standard library** - Don't test Go's built-in functionality
- **Focus on your logic** - Test the decisions and transformations your code makes
## Patterns to Follow
### Common principles
- Avoid static variables shared across tests
- Use random data when possible, use faker (github.com/go-faker/faker/v4)
- Don't pollute testing namespace - if helper functions are only used within one test, nest them inside that test function
- Compare entire structs when possible instead of individual fields (e.g `assert.Equal(t, expectedUser, actualUser)`)
- Use factory functions to create reusable random data
### IMPORTANT: Always Use Randomized Data
Never use static strings or values in tests unless absolutely necessary. Always use faker to generate random data:
```go
// DON'T DO THIS:
user := "john_doe" // Static name
userID := 1234567890 // Static ID
// DO THIS:
user := "user-" + faker.Username() // Randomized name
userID := 100 + rand.IntN(10000) // Randomized ID
```
If static data is absolutely required (e.g., for testing specific edge cases or validation rules), add a comment explaining why:
```go
// Static value required here because we're testing the specific validation rule for reserved usernames
user := "admin" // "admin" is a reserved name in our system
```
### Use factory functions to create reusable random data
**Only** if you need to reuse the same structure in multiple tests, create a factory function with options pattern to reuse it.
```go
// The file should be named similar to `accounts_testing.go`
// and have "//go:build !release" tag to avoid including it in the release build
// Define option function type
type RandomUserOpt func(*User)
// Create option functions only for those fields that need to be
// assigned in the test
func WithRandomUserID(id string) RandomUserOpt {
return func(u *User) {
u.ID = id
}
}
// Create generator function
func NewRandomUser(opts ...RandomUserOpt) User {
user := User{
ID: faker.UUIDHyphenated(),
Name: faker.Name(),
}
// Apply all options
for _, opt := range opts {
opt(&user)
}
return user
}
// In tests
id := faker.UUIDHyphenated()
user := NewRandomUser(WithRandomUserID(id))
```
### Avoid Redundant Test Data Generation
When using test factory functions, leverage their default values instead of overriding them unnecessarily:
```go
// DON'T DO THIS: Overriding values that are already generated by default
expectedPR := bitbucket.NewRandomPullRequest(
bitbucket.WithPullRequestID(100 + rand.IntN(10000)), // Already generated
bitbucket.WithPullRequestTitle("PR-" + faker.Sentence()), // Already generated
// ...
)
// DO THIS: Use the defaults and only override what you actually need
expectedPR := bitbucket.NewRandomPullRequest()
// Only override when you need specific values for your test
expectedPR := bitbucket.NewRandomPullRequest(
bitbucket.WithPullRequestState("MERGED"), // Only override what's different
)
```
### Use Struct Comparison
Compare entire structs instead of individual fields:
```go
// Instead of this:
assert.Equal(t, expectedUser.ID, actualUser.ID)
assert.Equal(t, expectedUser.Name, actualUser.Name)
// Do this:
assert.Equal(t, expectedUser, actualUser)
```
### Use makeMockDeps()
If your component has dependencies, use pattern below:
```go
func TestMyService(t *testing.T) {
// Use explicit t parameter if mock constructor requires it
// do not use top-level t parameter
makeMockDeps := func(t *testing.T) MyServiceDeps {
rootLogger := diag.RootTestLogger().With("test", t.Name())
return MyServiceDeps{
Repository: NewMockMyRepository(t),
RootLogger: rootLogger,
}
}
t.Run("some test", func(t *testing.T) {
deps := makeMockDeps(t)
// Use mocks.GetMock from testing/mocks package to get mocked instance
mockRepo := mocks.GetMock[*MockMyRepository](t, deps.Repository)
// ... rest of the test
})
}
```
**Important**: Use explicit `t *testing.T` parameter in `makeMockDeps` to ensure each test gets its own mock instance with proper cleanup.
### Use Mockery EXPECT() Pattern
When using mockery-generated mocks, prefer the `EXPECT()` pattern over the older `On()` pattern:
```go
// DO THIS: Use EXPECT() for better type safety and cleaner syntax
mockRepo.EXPECT().GetUser(t.Context(), userID).Return(expectedUser, nil)
// DON'T DO THIS: Older On() pattern
mockRepo.On("GetUser", mock.Anything, userID).Return(expectedUser, nil)
```
### Check for errors properly
Use below pattern to check for errors:
```go
require.NoError(t, err) // If you expect no error
require.Error(t, err) // If you expect an error
// Prefer this over assert.Contains(t, err.Error(), expectedError.Error())
assert.ErrorIs(t, err, expectedError)
```
### Follow TDD Strictly
When implementing new features, always follow the TDD cycle strictly:
1. **Write failing test first** - Test should fail because the behavior doesn't exist yet
2. **Run test to confirm it fails** - This validates your test is actually testing something
3. **Implement minimal code** - Write the smallest amount of code to make the test pass
4. **Run test to confirm it passes** - Verify your implementation works
5. **Refactor if needed** - Clean up the code while keeping tests green
6. **Repeat** - Add more test cases and implement more behavior
**Example TDD cycle:**
```go
// Step 1: Write failing test
func TestMyService(t *testing.T) {
t.Run("should return error when account not found", func(t *testing.T) {
// Test expects specific error behavior
// This will fail because implementation returns nil, nil or stub data
})
}
// Step 2: Run test - it fails ✓
// Step 3: Implement minimal code to make test pass
// Step 4: Run test - it passes ✓
// Step 5: Add more test cases and repeat
```
## Test Structure
### IMPORTANT: Use a Single Top-level Test Function Per Component
Always use a single top-level test function named after the component being tested, with nested t.Run blocks for each method or behavior:
```go
/// DO THIS: Single top-level function with nested tests
func TestUserRepository(t *testing.T) {
t.Run("should handle valid input", func(t *testing.T) {
// Test the main functionality
})
t.Run("should handle missing data", func(t *testing.T) {
// Test edge case that matters to business logic
})
}
// DON'T DO THIS: Multiple top-level functions
// func TestUserRepository_GetByID(t *testing.T) { ... }
// func TestUserRepository_Create(t *testing.T) { ... }
```
This approach:
- Keeps all tests for a component organized in one place
- Reduces duplication of setup code
- Makes it easier to run all tests for a component
- Follows the project's established testing conventions
### Follow AAA Pattern
- **Arrange** - Set up test data and mocks
- **Act** - Call the code under test
- **Assert** - Verify the expected behavior
## Common Mistakes to Avoid
1. **Over-testing** - Testing every possible combination when only a few matter
2. **Testing implementation details** - Checking internal state instead of behavior
3. **Complex mocks** - Over-engineered mock implementations
4. **Testing the framework** - Verifying that standard library functions work
5. **Inconsistent patterns** - Not following established codebase conventions
6. **Redundant test data** - Overriding default values that are already provided by test utilities
## Remember
> "Test the code you wrote, not the code you didn't write."
Focus on your business logic and keep tests simple, relevant, and maintainable.
### Mocking with Mockery
Add required interfaces to [.mockery.yaml](../.mockery.yaml) and run `mockery` (from root folder) to generate mocks.