Add gomock Integration for Fast Unit Testing
#17 opened on Oct 6, 2025
Repository metrics
- Stars
- (60 stars)
- PR merge metrics
- (PR metrics pending)
Description
🎯 Goal
Integrate gomock for generating mock implementations of interfaces, enabling fast, isolated unit tests without database dependencies.
📋 Description
Currently, testing the service layer requires a real database connection (SQLite in tests). While integration tests are valuable, we need fast unit tests that can run in milliseconds without external dependencies. gomock will automatically generate mock implementations of our repository interfaces.
Benefits:
- ⚡ Fast tests - Run in milliseconds (no database I/O)
- 🎯 Isolated tests - Test only the service logic
- 🧪 Easy edge cases - Simulate errors, timeouts, race conditions
- 🔄 Better CI/CD - Faster feedback loops
- 📚 Best practices - Industry-standard testing pattern
This follows patterns from successful Go projects like qiangxue/go-rest-api and aligns with our Docker-first philosophy.
✅ Acceptance Criteria
1. Install gomock in Dockerfile
- Add mockgen to development stage in
Dockerfile:
# Development stage
FROM golang:1.23-alpine AS dev
# Install development tools
RUN apk add --no-cache git postgresql-client make
# Install Go development tools
RUN go install github.com/air-verse/air@latest && \
go install github.com/swaggo/swag/cmd/swag@latest && \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest && \
go install github.com/golang/mock/mockgen@latest
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Default command for development
CMD ["air", "-c", ".air.toml"]
Important: Do NOT add mockgen to production stage (keeps image minimal).
2. Add gomock Dependency
- Add to
go.mod:
go get github.com/golang/mock@latest
3. Create Mock Directory Structure
- Create mock directories:
mkdir -p internal/user/mock
touch internal/user/mock/.gitkeep
4. Add Makefile Commands
- Add mock generation commands to
Makefile:
# ==================================================================================== #
# MOCK GENERATION
# ==================================================================================== #
## mock: Generate mocks for testing
.PHONY: mock
mock:
ifeq ($(IS_DOCKER),true)
@echo "$(BLUE)Generating mocks in Docker...$(NC)"
mockgen -source=internal/user/repository.go \
-destination=internal/user/mock/repository.go \
-package=mock
@echo "$(GREEN)✓ Mocks generated successfully$(NC)"
else
@echo "$(BLUE)Generating mocks on host...$(NC)"
@if ! command -v mockgen > /dev/null; then \
echo "$(YELLOW)mockgen not found. Installing...$(NC)"; \
go install github.com/golang/mock/mockgen@latest; \
fi
mockgen -source=internal/user/repository.go \
-destination=internal/user/mock/repository.go \
-package=mock
@echo "$(GREEN)✓ Mocks generated successfully$(NC)"
endif
## mock-all: Generate all mocks (add more as needed)
.PHONY: mock-all
mock-all: mock
@echo "$(GREEN)✓ All mocks generated$(NC)"
## mock-clean: Remove generated mock files
.PHONY: mock-clean
mock-clean:
@echo "$(BLUE)Cleaning generated mocks...$(NC)"
find . -path "*/mock/*.go" ! -name ".gitkeep" -type f -delete
@echo "$(GREEN)✓ Mocks cleaned$(NC)"
5. Update .gitignore
- Add mock files to
.gitignore:
# Mock files (generated by gomock)
**/mock/*.go
!**/mock/.gitkeep
Note: Some projects commit mocks to git, others regenerate them. For this project, we'll regenerate them (keeps git history clean).
6. Ensure Repository Interface Exists
- Verify
internal/user/repository.gohas proper interface:
package user
// Repository defines the interface for user data access
type Repository interface {
Create(user *User) error
GetByID(id uint) (*User, error)
GetByEmail(email string) (*User, error)
Update(user *User) error
Delete(id uint) error
}
If the interface doesn't exist, extract it from the concrete implementation.
7. Generate Mocks
- Run mock generation:
make mock
This should create internal/user/mock/repository.go with content like:
// Code generated by MockGen. DO NOT EDIT.
// Source: internal/user/repository.go
package mock
import (
gomock "github.com/golang/mock/gomock"
user "github.com/vahiiiid/go-rest-api-boilerplate/internal/user"
)
// MockRepository is a mock of Repository interface
type MockRepository struct {
ctrl *gomock.Controller
recorder *MockRepositoryMockRecorder
}
// ... generated mock methods
8. Create Example Unit Test with Mocks
- Create
internal/user/service_unit_test.go:
package user
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/vahiiiid/go-rest-api-boilerplate/internal/user/mock"
"golang.org/x/crypto/bcrypt"
)
func TestService_GetByID_Success(t *testing.T) {
// Setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl)
service := NewService(mockRepo)
// Expected user
expectedUser := &User{
ID: 1,
Name: "Alice Smith",
Email: "alice@example.com",
}
// Mock expectation
mockRepo.EXPECT().
GetByID(uint(1)).
Return(expectedUser, nil).
Times(1)
// Execute
result, err := service.GetByID(1)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedUser.ID, result.ID)
assert.Equal(t, expectedUser.Name, result.Name)
assert.Equal(t, expectedUser.Email, result.Email)
}
func TestService_GetByID_NotFound(t *testing.T) {
// Setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl)
service := NewService(mockRepo)
// Mock expectation: return error
mockRepo.EXPECT().
GetByID(uint(999)).
Return(nil, errors.New("user not found")).
Times(1)
// Execute
result, err := service.GetByID(999)
// Assert
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "user not found")
}
func TestService_Register_EmailAlreadyExists(t *testing.T) {
// Setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl)
service := NewService(mockRepo)
existingUser := &User{
ID: 1,
Email: "alice@example.com",
}
// Mock: Email check returns existing user
mockRepo.EXPECT().
GetByEmail("alice@example.com").
Return(existingUser, nil).
Times(1)
// Execute
_, err := service.Register(RegisterRequest{
Name: "Alice",
Email: "alice@example.com",
Password: "password123",
})
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "email already exists")
}
func TestService_Register_Success(t *testing.T) {
// Setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl)
service := NewService(mockRepo)
// Mock: Email doesn't exist
mockRepo.EXPECT().
GetByEmail("newuser@example.com").
Return(nil, nil).
Times(1)
// Mock: Create succeeds
mockRepo.EXPECT().
Create(gomock.Any()).
Return(nil).
Times(1)
// Execute
user, err := service.Register(RegisterRequest{
Name: "New User",
Email: "newuser@example.com",
Password: "password123",
})
// Assert
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "New User", user.Name)
assert.Equal(t, "newuser@example.com", user.Email)
// Verify password is hashed
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte("password123"))
assert.NoError(t, err, "Password should be hashed")
}
func TestService_Delete_Unauthorized(t *testing.T) {
// Setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl)
service := NewService(mockRepo)
// Mock: User exists but belongs to someone else
mockRepo.EXPECT().
GetByID(uint(1)).
Return(&User{ID: 1}, nil).
Times(1)
// Execute: Try to delete as different user
err := service.Delete(1, 999) // userID=1, requestingUserID=999
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "forbidden")
}
9. Update Test Running
- Ensure unit tests run separately from integration tests:
## test-unit: Run unit tests with mocks (fast)
.PHONY: test-unit
test-unit:
ifeq ($(IS_DOCKER),true)
@echo "$(BLUE)Running unit tests in Docker...$(NC)"
go test -v -short ./internal/...
else
@echo "$(BLUE)Running unit tests on host...$(NC)"
go test -v -short ./internal/...
endif
## test-integration: Run integration tests (with database)
.PHONY: test-integration
test-integration:
ifeq ($(IS_DOCKER),true)
@echo "$(BLUE)Running integration tests in Docker...$(NC)"
go test -v ./tests/...
else
@echo "$(BLUE)Running integration tests on host...$(NC)"
go test -v ./tests/...
endif
## test: Run all tests
.PHONY: test
test: test-unit test-integration
10. Update help Command
- Add mock commands to Makefile help:
mock Generate mocks for testing
mock-all Generate all mocks
mock-clean Remove generated mock files
test-unit Run fast unit tests (with mocks)
test-integration Run integration tests (with database)
11. Testing the Implementation
- Verify mockgen is available in Docker:
make up
docker-compose exec app which mockgen
# Should output: /go/bin/mockgen
- Generate mocks:
make mock
# Should create internal/user/mock/repository.go
- Run unit tests:
make test-unit
# Should run in < 1 second
- Run all tests:
make test
# Should run both unit and integration tests
12. Documentation Updates
Update README.md
- Add testing section:
## 🧪 Testing
### Running Tests
```bash
# Run all tests (unit + integration)
make test
# Run only fast unit tests (with mocks, no database needed)
make test-unit
# Run only integration tests (requires database)
make test-integration
# Generate mocks for testing
make mock
# Clean generated mocks
make mock-clean
Test Types
- Unit Tests (
internal/*/service_unit_test.go) - Fast tests with mocked dependencies - Integration Tests (
tests/*_test.go) - Full-stack tests with real database
Generating Mocks
This project uses gomock for generating test mocks:
# Generate all mocks
make mock
# Mocks are auto-generated from interfaces and placed in */mock/ directories
# They are gitignored and regenerated as needed
#### Update Development Guide
- [ ] Add section on writing tests with mocks
- [ ] Provide examples of different mock scenarios
- [ ] Explain when to use unit vs integration tests
### 13. GitHub Actions CI Update
- [ ] Update `.github/workflows/ci.yml` to generate mocks before tests:
```yaml
- name: Generate mocks
run: make mock
- name: Run unit tests
run: make test-unit
- name: Run integration tests
run: make test-integration
💡 Usage Examples
Before (Integration Test Only)
func TestUserService(t *testing.T) {
// Need real database
db := setupTestDB() // Slow (100ms+)
repo := user.NewRepository(db)
service := user.NewService(repo)
// Test with real DB queries
}
After (Fast Unit Test with Mock)
func TestUserService(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockRepository(ctrl) // Fast (<1ms)
mockRepo.EXPECT().GetByID(1).Return(&user, nil)
service := user.NewService(mockRepo)
// Test only service logic
}
📊 Performance Comparison
| Test Type | Time | Dependencies | What It Tests |
|---|---|---|---|
| Integration | 5-10s | Docker, PostgreSQL | Full stack |
| Unit (with mocks) | <1s | None | Service logic only |
📚 Resources
- gomock Documentation
- Testing with Mocks in Go
- qiangxue/go-rest-api testing example
- Effective Go: Testing
🎓 Difficulty Level
Intermediate - Requires understanding of interfaces, dependency injection, and testing patterns.
🔗 Related PRs/Issues
- Complements existing integration tests in
tests/ - Works with Docker-first development philosophy
- Prepares for structured error handling (#12)
✅ Definition of Done
- mockgen installed in Dockerfile dev stage
- Makefile commands (
mock,mock-clean,test-unit) working - Mock directory structure created
- At least 3 example unit tests with mocks
- Unit tests run in < 1 second
- Integration tests still work
- Documentation updated (README + Development Guide)
- CI pipeline generates mocks and runs tests
- All existing tests still pass
-
make helpshows new commands
🚀 Quick Start (After Implementation)
# Start development environment
make quick-start
# Generate mocks
make mock
# Run fast unit tests (no database needed!)
make test-unit
# Run all tests
make test
Note: This follows the Docker-first philosophy of GRAB - all tools pre-installed, make commands auto-detect environment, and everything works out of the box. Keep both unit tests (fast feedback) and integration tests (confidence) for best coverage!