Unit Tests
Fast, isolated tests that verify your business logic without external dependencies like databases or message brokers.
Unit tests verify isolated pieces of logic without relying on external infrastructure. They don't need a database, a message broker, or any other service running. This makes them fast and reliable.
In a well-structured application, unit tests cover the domain and application layers. The domain layer is where you have the most complex business rules, so it benefits the most from thorough testing. If the domain model is designed well (with methods that enforce invariants and return errors for invalid operations), tests are straightforward: call a method, check the result. No mocking needed.
The application layer sometimes needs mocked dependencies, but only when there's real orchestration logic worth testing. If a command handler only calls a repository and publishes an event, testing it doesn't add much value. Focus on the cases where the logic is non-trivial.
Clean Architecture and Dependency Injection make unit testing practical. When your business logic doesn't depend on infrastructure details, you can test it in isolation. Without these patterns, you often end up needing a running database just to test a simple rule.
Integration tests and component tests complement unit tests by covering what they can't: real database queries, message delivery, and full request lifecycles. A good test suite uses all three levels, with unit tests forming the fast, broad base.
References
- Microservices test architecture. Can you sleep well without end-to-end tests? — Dedicates a section to unit tests as the base of the testing pyramid, covering domain and application layers. Explains when application-layer unit tests add value and when they don't.
- How to implement Clean Architecture in Go (Golang) — Shows how Clean Architecture makes unit testing practical. Before the refactoring, the only way to test the code was with integration tests requiring a running database. After introducing layers, the same logic was covered with a unit test suite.
- Introduction to DDD Lite: When microservices in Go are not enough — Demonstrates how a well-designed domain model makes unit tests straightforward with no mocking needed. Shows working on the domain layer for weeks with just in-memory storage and unit tests before choosing a database.
- 4 practical principles of high-quality database integration tests in Go — Discusses the reversed test pyramid for infrastructure-heavy code where unit tests can't cover enough functionality, and recommends building non-unit tests to run in parallel.
- Database Transactions in Go with Layered Architecture — Shows how leaking SQL transactions through layers forces what should be a simple unit test into an integration test, and how Clean Architecture avoids this problem.
- Optimising and Visualising Go Tests Parallelism: Why more cores don't speed up your Go tests — Warns that using t.Parallel() for lightweight unit tests will likely make them slower due to overhead. The default parallel values work well for many lightweight unit tests.
- Increasing Cohesion in Go with Generic Decorators — Shows how extracting cross-cutting concerns into decorators makes the core logic unit-testable without needing to mock loggers or metrics clients.
- Running integration tests with docker-compose in Google Cloud Build — Shows using build tags to separate unit tests from Docker-dependent tests in CI, running unit tests as a fast first guard against mistakes.
- Is Clean Architecture Overengineering? — Discusses how unit tests are a great fit for the domain layer because it's purely logic with few dependencies, while the application layer usually doesn't benefit from extensive unit testing.
- Unpopular opinions about Go — Argues against using mocking libraries in unit tests because they break when you change logic. Recommends writing stubs instead, which avoids thread-safety issues in parallel tests.