Yeda AI Tips · #070

Español

Don't Trust In-Memory Database Tests

Your in-memory database tests are lying to you. An in-memory fake never translates your queries into the SQL dialect your production database speaks — it evaluates them as plain in-process operations. So a query can pass every test on your machine and still fail, or silently return different rows, the first time it hits the real engine.

Why green tests break in production

An ORM query is really two programs: the query you wrote, and the SQL your provider translates it into. An in-memory fake runs only the first one. Everything that lives in the translation layer goes untested:

Microsoft's own EF Core docs are blunt about it: using the in-memory provider as a test fake is "highly discouraged," and it's kept only for legacy applications.

The fix: test the risky queries against a real engine

"Real database tests are slow and painful" was true in 2015. Today the setup cost is one container:

// Testcontainers: a real PostgreSQL, per test run, auto-cleaned
var pg = new PostgreSqlBuilder().Build();
await pg.StartAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseNpgsql(pg.GetConnectionString())
    .Options;

Testcontainers (available for .NET, Java, Go, Node, Python, and more) boots the same engine you run in production inside Docker, hands your tests a connection string, and tears it down afterwards. The EF Core team's own suite runs 30,000+ tests against real SQL Server in a few minutes, on every commit — a local database with a reasonable dataset is fast, because everything stays on one machine.

Isolation is the real design work, not speed. The standard recipe: create and seed the database once per test run (a shared fixture), wrap each data-modifying test in a transaction you never commit, and give tests that must commit their own database plus a cleanup step.

Rules of thumb

SituationTest against
Pure business logic, no queriesNo database — stub the data layer
LINQ/ORM queries, joins, filtersReal engine (containerized)
Raw SQL anywhere in the pathReal engine — fakes can't run it
Transactions, rollback behaviorReal engine — in-memory ignores them
Migrations before a releaseReal engine, production version
You truly need a fast fakeSQLite in-memory over the ORM's in-memory provider — and know its dialect differs too

Power-user notes

Keep in-memory (or better, stubbed repositories) for speed on pure logic — but prove every risky query, migration, and transaction against the real engine before it ships.

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog