Stage 1 · Code
Working with Databases
Testing Data Access Code
Database code deserves the same confidence as any other part of your program. This lesson shows how to combine fast tests, interface-driven fakes, and disposable real-database checks without turning testing into a chore.
Start with Fast Feedback
When database tests are slow, fragile, or painful to set up, teams quietly stop running them. That is why testing data-access code is partly a technical problem and partly a workflow problem. Good tests should help you change code confidently, not punish you for caring about correctness.
A healthy strategy usually has layers. The fast layer uses something lightweight: an in-memory or test-only database, or a fake implementation that stays in Go memory and returns predictable results. That layer catches logic mistakes quickly. The slower layer uses a real disposable database to verify SQL syntax, schema assumptions, and behavior that only an actual database can prove.
A fake repository is excellent for testing business logic. A disposable real database is excellent for catching bad SQL and schema drift. Strong teams usually want both, not a false choice between them.
Interfaces Make Swapping Possible
This is where your earlier Go interface knowledge suddenly becomes practical. If the rest of your program depends on a small repository interface instead of a concrete SQL implementation, tests can swap in a fake without dragging a database server into every run. The service under test stays focused on behavior, not infrastructure setup.
Notice how this connects back to interfaces as contracts. The service code can say, 'I need something that can save and load notes.' It does not need to care whether that something talks to PostgreSQL, a lightweight test database, or a Go map protected by a mutex.
When Real Database Tests Are Worth It
Fakes are fast, but they cannot tell you whether your SQL is valid, whether a migration created the columns you think it did, or whether scanning and transactions behave correctly against a real engine. That is why integration tests still matter. The trick is to keep them disposable and clearly separated from the instant-feedback tests.
- Use a test-only database instance or container, never a shared production-like database full of unrelated data.
- Apply the same migrations your application uses so the schema is realistic.
- Seed only the rows the test needs, then clean up or recreate the database so tests stay independent.
- Run these tests in CI and before bigger refactors, even if you skip them during every tiny local edit.
Tracing a Fake Repository Test
Quiz and Practice
Why use a repository interface in tests?
Hands-On Project
Implement `ByID` for the in-memory repository. It should return the note when present and `ErrNoteNotFound` when missing.
Summary and Key Takeaways
- Testing data access usually works best as layers: fast logic tests plus slower real-database integration tests.
- Repository interfaces let you swap concrete storage implementations cleanly in tests.
- A fake in-memory repository is ideal for service-level behavior that does not need real SQL.
- Disposable integration databases catch mistakes that fakes never can, such as broken SQL or drifted schema assumptions.
- Test speed matters because slow, painful tests are the tests teams quietly stop running.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.