Should I test that?

Should I test ORM code?

Verdict

Yes

Test the queries you write through an ORM with integration tests against the database engine production runs; do not test the ORM library itself, and do not test queries against a mocked DbContext or an in-memory provider.

Why

Test the queries you write through an ORM, against the database engine production runs. The typical case is a backend whose code builds LINQ queries for Entity Framework Core, QuerySets for Django or JPA queries for Hibernate. Blast radius is users and Change frequency is regularly, because customers see the rows a query returns, and queries gain filters with most features. Detectability is eventually, because a wrong filter or a missing Include returns plausible rows and no error. Reversibility is with-effort, because code acts on the wrong rows until a hotfix, and Test cost is moderate, because each test needs a local database, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
Entities map columns by naming convention, and CI starts the app with Hibernate schema validation (ddl-auto=validate)Do not write mapping tests for these entitiesDetectability moves to immediately and Reversibility to trivial, because a missing column fails startup in CI and a convention leaves no hand-written name to swap
Queries return only the current tenant's rows through a global filter such as EF Core HasQueryFilterTest mandatory, including a query from one tenant that must not return another tenant's rowsBlast radius rises to safety-or-legal and Detectability to never, because a missing filter shows another customer's records without an error
A query sums invoice lines into the amount a customer is chargedTest mandatory: the main path, cancelled and discounted lines, and the boundary amountsBlast radius rises to money and Reversibility to costly, because a wrong charge needs refunds
The ORM code is a one-time backfill that rewrites existing rowsTest it differently: run it on a copy of production data and compare row counts and sample rows before and afterChange frequency falls to once while Blast radius stays users and Reversibility with-effort

What breaks if you don't test

ORM code breaks between the code you read and the SQL that runs. A string comparison in a LINQ Where is case-insensitive under SQL Server's default collation and case-sensitive in SQLite, so one query returns different rows on the two engines. A query that forgets Include returns orders without their lines. Neither raises an error, and users report the missing data days later.

What you lose if you over-test

The common over-test mocks DbSet: the LINQ then runs over a list in .NET instead of as SQL, so the test passes for a query the database translates differently. Another tests the ORM itself; EF Core already runs over 30,000 tests against SQL Server, according to Microsoft's testing guide. Both stay green when the SQL is wrong.

How to test

Write one integration test per query, against the production engine started in a container. Seed rows the query must return and rows it must skip, so a wrong filter changes the result. Seed through one context and query through a fresh one, because EF Core fills navigation properties from entities the context already tracks. Microsoft's guide to testing against your database shows how to isolate such tests, and Django builds its test database on the engine set in DATABASES.

When the answer changes

  • A query decides which tenant's or which user's rows come back.
  • A query computes an amount that a customer pays.
  • The ORM code runs once, as a backfill.

Real incident + Code example

The agenda that lost its speakers

On an ASP.NET Core app for conference organizers I worked on, a refactor moved the agenda query into a repository and dropped .Include(s => s.Speakers). The tests used the EF Core in-memory provider and seeded sessions through the same DbContext the repository queried. EF Core fixes up navigation properties to entities already loaded into the context, so each test session still had its speakers. In production each request got a fresh context, and the public agenda listed 140 sessions with no speakers. An organizer noticed four days later. We restored the Include and moved the tests to SQL Server in a container:

public class AgendaQueryTests(SqlServerFixture db) : IClassFixture<SqlServerFixture>
{
    [Fact]
    public async Task Agenda_loads_the_speakers_of_each_session()
    {
        await using (var seed = db.CreateContext())
        {
            seed.Sessions.Add(new Session
            {
                Title = "Keynote",
                Speakers = [new Speaker { Name = "Ana Ruiz" }]
            });
            await seed.SaveChangesAsync();
        }

        // A fresh context tracks no speakers that could hide a missing Include.
        await using var context = db.CreateContext();
        var agenda = await new AgendaRepository(context).GetAgendaAsync();

        Assert.Equal("Ana Ruiz", agenda.Single().Speakers.Single().Name);
    }
}

FAQ

Unit testing with Entity Framework: should you bother?

Yes, test the Entity Framework queries you write, with integration tests against a real database instead of unit tests with a mocked DbContext. A mock runs LINQ over a list in memory, so it passes for queries that SQL Server translates differently.

Should methods containing LINQ expressions be tested?

Yes, test each LINQ query that runs against a database with one integration test that seeds rows it must return and rows it must skip. LINQ over a plain list follows C# rules and needs a unit test only when it holds logic.

Should I mock DbContext and DbSet?

No, do not mock DbSet to test queries, because LINQ operators are extension methods that run in memory against the mock. Test queries against a real database, and stub your own repository interface when you test the code above it.

Should I use the EF Core in-memory provider for tests?

No, run the tests against the production database engine in a container. Microsoft calls the in-memory provider highly discouraged for testing: it supports neither transactions nor raw SQL, and it compares strings differently from SQL Server.