Verdict
Yes
Test each query in the repository layer with an integration test against the database engine production runs, using rows that each filter must return and rows it must leave out; do not unit test repositories with a mocked database.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Test every query in the repository layer, against the database engine production runs. The typical case is a backend where each repository holds the queries for one entity and a service calls them. Blast radius is users and Change frequency is regularly, because customers see the rows these queries return, and most features add a filter. Detectability is eventually, because a wrong condition returns plausible rows without an error. Reversibility is with-effort, because code acts on the wrong rows until someone repairs them, and Test cost is moderate, because each test needs a database in a container, so rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The repository only inherits save, findById and delete from a framework base such as Spring Data CrudRepository | Do not test the repository; test the code that calls it | Change frequency falls to rarely, Detectability to immediately and Reversibility to trivial, because the framework's own suite covers these methods |
| Endpoint tests run each repository query against a real database, with rows that each filter must leave out | Do not add separate repository tests | Detectability moves to immediately and Reversibility to trivial, because a wrong filter fails those tests in CI |
| Every repository query adds the tenant ID, and several companies share one database | Test mandatory: records of two tenants in each test, checking that each query returns one tenant's rows | Blast radius rises to safety-or-legal and Detectability to never, because a missing tenant filter leaks records without an error |
| A repository query selects the subscriptions that the nightly billing job charges | Test mandatory: the main path, every known failure and the date boundaries | Blast radius rises to money and Reversibility to costly, because a double charge needs a refund |
| A read-only query feeds an internal staff dashboard, and no code acts on its rows | Test minimally: one database test of the main filter | Blast radius falls to internal and Reversibility to trivial |
| The queries live in stored procedures that tests can reach only through a shared staging database | Test it differently: a nightly job compares row counts with a reference query and alerts on a difference | Test cost rises to heavy while Detectability stays eventually and Reversibility with-effort |
What breaks if you don't test
Repository bugs sit in the SQL, and wrong SQL raises no error. An Or in place of an And, or a join that repeats a row for each child record, returns a list that looks complete. Service tests mock the repository, so they stay green. A customer notices weeks later, when a record they know exists is missing from a list.
What you lose if you over-test
The common over-test mocks the EntityManager, DbSet or JDBC connection and asserts the query string. A mocked repository test passes when the SQL is wrong, because the SQL never runs, and fails when you rename a parameter. A test of the inherited save() and findById() checks Spring Data or EF Core instead of your code.
How to test
Write one integration test per query method, against the production database engine started with Testcontainers. Insert rows the query must return and rows it must leave out, including one exactly on each boundary, such as midnight at both ends of a date range. Assert the exact set of returned IDs, since a count passes when a wrong row replaces a right one. Spring Boot's @DataJpaTest test slice starts only the persistence layer; for .NET, follow the EF Core testing guidance.
When the answer changes
- The repository declares no query of its own.
- A query filters by tenant or owner.
- A query selects the records that a job charges or pays out.
Real incident + Code example
The report that lost the last day of each month
On a property-management product I worked on, landlords received a monthly PDF of maintenance requests. The repository ran a native query with created_at BETWEEN :from AND :to, called with the first and last day of the month as LocalDate values. PostgreSQL treats a date as midnight when it compares one with a timestamp, so requests created on the last day after 00:00 were left out. The service test mocked the repository and passed. Four reports went out before a landlord asked why a burst pipe reported on 31 January was missing, and we regenerated them for about 120 landlords. The fix changed the upper bound to created_at < CAST(:to AS date) + 1, with this test:
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class MaintenanceRequestRepositoryTest {
@Container @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired MaintenanceRequestRepository requests;
@Test
void monthIncludesItsLastDayAndNothingOutsideIt() {
requests.saveAndFlush(new MaintenanceRequest(7, LocalDateTime.parse("2026-02-28T23:59:00")));
var first = requests.saveAndFlush(new MaintenanceRequest(7, LocalDateTime.parse("2026-03-01T00:00:00")));
var last = requests.saveAndFlush(new MaintenanceRequest(7, LocalDateTime.parse("2026-03-31T18:30:00")));
requests.saveAndFlush(new MaintenanceRequest(7, LocalDateTime.parse("2026-04-01T00:00:00")));
var march = requests.findCreatedBetween(7, LocalDate.of(2026, 3, 1), LocalDate.of(2026, 3, 31));
assertThat(march).extracting(MaintenanceRequest::getId)
.containsExactlyInAnyOrder(first.getId(), last.getId());
}
}
Related questions
FAQ
- Should I unit test repositories?
No, test repositories with integration tests against a real database instead of unit tests with a mocked
EntityManager. A mocked database never runs the SQL, where a wrong filter or join sits.- Should DAOs be unit tested?
No, test a DAO with one integration test per query against the production database engine instead of a mocked JDBC connection. A test with a mocked connection repeats the SQL string and passes when the SQL is wrong.
- Should I test Spring Data JPA repositories?
Yes, test each query method you declare in a Spring Data JPA repository, derived or written in
@Query, with one@DataJpaTestagainst the production database engine. Skip the inheritedsave,findByIdanddelete, which Spring Data's own suite covers.- Should repository tests use H2 or an in-memory database?
No, run repository tests on the database engine production runs, in a container. H2's documentation says its compatibility modes cover only a small subset of the differences between databases, so a query that passes on H2 can fail on PostgreSQL.