No confíes en los tests con base de datos en memoria
Tus tests con base de datos en memoria te están mintiendo. Un fake en memoria nunca traduce tus consultas al dialecto SQL que habla tu base de datos de producción: las evalúa como operaciones en proceso. Por eso una consulta puede pasar todos los tests en tu máquina y aun así fallar, o devolver filas distintas en silencio, la primera vez que toca el motor real.
Por qué los tests en verde se rompen en producción
Una consulta de ORM son en realidad dos programas: la consulta que escribiste y el SQL al que el proveedor la traduce. Un fake en memoria ejecuta solo el primero. Todo lo que vive en la capa de traducción queda sin probar:
- Reglas de colación y comparación. SQL Server compara cadenas sin distinguir mayúsculas por defecto; una colección en memoria (y SQLite) sí las distingue. Una búsqueda que encuentra
"Alice"en tests no encuentra"alice"en producción — o al revés. - Funciones específicas del proveedor. Una llamada como
EF.Functions.DateDiffDayde SQL Server se traduce bien en el proveedor real y simplemente falla en un fake. - SQL crudo. Los fakes en memoria no pueden ejecutarlo, así que cualquier consulta escrita a mano es un punto ciego.
- Transacciones. El proveedor en memoria de EF Core las ignora: un test de rollback pasa sin haber revertido nada.
- Migraciones y esquema. Un fake no tiene esquema que migrar, así que los errores de constraints, índices y tipos de columna aparecen recién al desplegar.
La propia documentación de EF Core de Microsoft es directa: usar el proveedor en memoria como fake de pruebas está "altamente desaconsejado" y se mantiene solo para aplicaciones legadas.
La solución: probar las consultas riesgosas contra un motor real
"Los tests con base de datos real son lentos y dolorosos" era cierto en 2015. Hoy el costo de configuración es un contenedor:
// 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 (disponible para .NET, Java, Go, Node, Python y más) levanta en Docker el mismo motor que corres en producción, entrega a tus tests una cadena de conexión y lo limpia al terminar. La suite del propio equipo de EF Core ejecuta más de 30,000 tests contra SQL Server real en unos minutos, en cada commit: una base de datos local con un dataset razonable es rápida, porque todo queda en una sola máquina.
El verdadero trabajo de diseño es el aislamiento, no la velocidad. La receta estándar: crear y sembrar la base de datos una vez por corrida (un fixture compartido), envolver cada test que modifica datos en una transacción que nunca se confirma, y dar a los tests que sí deben confirmar su propia base de datos más un paso de limpieza.
Reglas prácticas
| Situación | Probar contra |
|---|---|
| Lógica de negocio pura, sin consultas | Sin base de datos: stub de la capa de datos |
| Consultas LINQ/ORM, joins, filtros | Motor real (en contenedor) |
| SQL crudo en cualquier punto | Motor real: los fakes no pueden ejecutarlo |
| Transacciones, comportamiento de rollback | Motor real: en memoria se ignoran |
| Migraciones antes de un release | Motor real, versión de producción |
| De verdad necesitas un fake rápido | SQLite en memoria antes que el proveedor en memoria del ORM — y su dialecto también difiere |
Notas para usuarios avanzados
- Si necesitas un doble, haz stub por encima del ORM. Una capa de repositorio que devuelve
IEnumerablepermite hacer stub de las salidas de la consulta en vez de re-ejecutar operadores en memoria: el único doble que no vuelve a probar la capa de traducción. - SQLite en memoria es el fake menos malo, no un test real. Sigue siendo otro dialecto, con otra colación y otro soporte de funciones; consultas que pasan ahí pueden fallar en tu motor de producción.
- Resetea rápido entre tests que confirman. Borrar y resembrar vía ORM es lento; un
DELETE FROMcrudo por tabla, o una librería de reset como Respawn, mantiene rápidas las suites con base real. - Iguala las versiones. Prueba contra la misma versión mayor que despliegas: un bug de traducción corregido en una versión del motor puede seguir vivo en la que realmente usas.
Conserva la memoria (o mejor, repositorios con stubs) para la velocidad en lógica pura — pero demuestra cada consulta riesgosa, migración y transacción contra el motor real antes de que llegue a producción.
Recursos
- Choosing a testing strategy — EF Core docs — la tabla comparativa en memoria vs. SQLite vs. base real, y por qué en memoria está "altamente desaconsejado".
- Testing against your production database system — EF Core docs — fixtures, aislamiento por rollback de transacción y patrones de limpieza.
- Testcontainers — Getting started — servicios reales en contenedores Docker para tests, con módulos por lenguaje.
- Avoid In-Memory Databases for Tests — Jimmy Bogard — el argumento de la falsa confianza, del autor de Respawn y AutoMapper.
- SQLite in-memory databases — cómo funciona realmente el modo
:memory:, si usas SQLite como fake.
¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.