abelcastro.dev

Django-style test DBs with Prisma: what I built in my monorepo (PoC)

2026-01-08

TypeScriptTestingPrismaDjango

Django-style test DBs in NestJS: my MikroORM integration test helper (PoC)

2025-09-11

TypeScriptTestingMikro-ORMDjango

Re‑building Sports Dashboard: From Angular + REST to Next.js + GraphQL

2025-05-07

TypeScriptNext.jsGraphQL
1
23
...
910

Abel Castro 2026 - checkout the source code of this page on GitHub - Privacy Policy

TL;DR

I wanted the same thing again: keep unit tests fast, but enable a subset of tests to run against a real Postgres database with migrations applied automatically similar to Django's default experience. In my monorepo PoC, I wired Prisma to support that workflow.

Note: This post describes a proof of concept. The core idea works, but I'll be explicit about what's still missing for a production-grade testing setup.

Django's test experience is integrated: test runner + ORM + migrations + DB lifecycle all come together. In the TS ecosystem, Prisma is “just” the ORM layer; you still need to decide:

  • How to initiate the DB
  • When migrations run
  • How to reset data
  • How to prevent parallel tests from stepping on each other

So I built a pragmatic approach inside my monorepo PoC.

Repo: abel-castro/blog-monorepo (NestJS API + frontend in one workspace). 

The approach (conceptually)

My Prisma integration-testing recipe is:

  1. Provide a real Postgres for tests - easiest path: docker-compose (local + CI)
  2. Use a dedicated DATABASE_URL for integration tests
    • never point integration tests at dev/prod DBs
    • ideally a separate DB name or schema
  3. Ensure schema is up-to-date before tests
    • run migrations programmatically, or via a pre-test command
    • be consistent: either “migrate” (schema history) or “db push” (fast, but less realistic)
  4. Reset data deterministically

Why Prisma feels different than Django here

Prisma is great, but it's intentionally not a full “framework”.

  • Prisma doesn't know your test runner (Jest/Vitest/node:test).
  • Prisma doesn't own your DI container (NestJS does).
  • Prisma migrations are a separate workflow (CLI / migration engine).
  • There's no universally “right” cleanup strategy.

So: you build a thin layer that standardizes your project's conventions.

What my PoC focuses on

This PoC focuses on the “Django feeling” of:

  • “I can write tests that hit a real DB”
  • “DB schema is ready when tests run”
  • “Unit tests remain fast and simple”

In other words: integration tests become easy to add.

Good points of this direction

  • More realistic tests for anything query-heavy:
  • filters, ordering, pagination
  • relations, constraints, unique indexes
  • migrations-related behavior
  • Less mocking around the ORM boundary
  • Higher confidence refactors (especially around query logic)

Current trade-offs

Even if the PoC works, there are predictable pain points:

  1. Speed
    • DB startup + migrations cost time.
    • Resetting DB between tests can dominate runtime if done naively.
  2. Isolation
    • The best isolation approaches add complexity (transactions, schemas, per-worker DBs).
    • The easiest approaches (shared DB, truncate) can allow ordering-dependent tests if you're sloppy.
  3. Parallel tests
    • Test workers + one DB can cause flakiness unless you isolate per worker.
  4. Developer friction
    • “Run DB before integration tests” has to be one command, or people won't use it.

What I'd implement next (turning PoC into a solid template)

If you want this to feel as boring as Django:

  1. A single command Something like:

    • pnpm test (unit only, no DB)
    • pnpm test:e2e (starts DB, migrates, runs integration specs, tears down)
  2. A robust reset strategy

  3. Per-worker DB isolation (parallel-safe)

    • create DB/schema names based on worker id
    • avoid cross-test collisions
  4. CI hardening

    • healthchecks
    • deterministic order
    • run integration tests in a separate job/stage

Closing

Django sets an expectation: “integration tests with a real DB should be trivial.” In Prisma + NestJS, that experience is absolutely achievable but you have to intentionally assemble the pieces.

Repo PoC: abel-castro/blog-monorepo.

TL;DR

Coming from Django, I missed the “it just works” test database story: tests can run real queries against a real DB with migrations applied automatically. In the TypeScript ecosystem, this is usually not “default behavior”, so I built a small helper for NestJS + MikroORM that gives me a Django-like workflow for integration tests while keeping unit tests fast and DB-free.

Background: what I miss from Django testing

In Django, the test runner and ORM are part of one cohesive stack:

  • A dedicated test database is created automatically.
  • Migrations (or schema setup) are handled for you.
  • Each test (or test class) gets isolation via transactions / database flush strategies.
  • The whole thing is standardized across most Django projects.

In many TS backends (NestJS + ORM + Jest/Vitest), the ecosystem is more modular. NestJS doesn’t own your ORM; the ORM doesn’t own your test runner; and the test runner doesn’t own your DB lifecycle. Result: you assemble your own conventions.

Why this isn’t “built-in” in many TS stacks (likely reasons)

A few reasons (none are “bad”, it’s mostly ecosystem shape):

  1. Framework/ORM/test-runner separation NestJS is framework-only; MikroORM/Prisma are external; Jest/Vitest are external. No single layer feels responsible for end-to-end test DB lifecycle.
  2. Performance expectations differ JS/ S culture often optimizes for fast unit tests first; integration tests exist but are intentionally explicit/opt-in.
  3. Parallelism & isolation are harder than they look A “shared test DB” is easy; a “shared test DB with parallel test workers, deterministic cleanup, and fast migrations” is not trivial to implement.

So instead of waiting for a universal solution, I created a small helper that matches my needs.

The PoC: DatabaseTest helper

In this PR, I added a tiny utility class that:

  • connects to a dedicated *_test database,
  • creates the DB if it doesn’t exist,
  • runs migrations,
  • and lets only certain test files opt in. 

Key idea: unit tests stay unit tests (fast, mocked). Integration tests opt into “real DB”.

The code

The helper lives at test/utils/database-test.ts and builds a MikroORM config from your app config, but with a test dbName and the migrator enabled. It ensures the database exists and migrates it.

Important implementation details from the PoC:

  • dbName becomes ${commonMikroOrmConfig.dbName}_test
  • extensions: [Migrator] enables migrations
  • ensureDatabase() creates the DB if missing
  • getMigrator().up() applies pending migrations
  • teardown closes the connection but keeps the database content

Using it in tests

I added posts.integration.spec.ts as a minimal integration test showing how it feels to use: 

  • beforeAll: orm = await DatabaseTest.init()
  • afterAll: await DatabaseTest.close()
  • create an entity, persistAndFlush, clear EM, query it back, assert.

This test demonstrates real persistence + retrieval via the DB. It also includes a second test that asserts data is still there (“keeps data between tests”). This is just here to demonstrate the default behavior but in a real world project it would be a good idea to delete the created data after each test case.

What I like about this approach (benefits)

  • Django-ish developer experience for integration tests: “run tests, DB exists, migrations applied”.
  • Opt-in integration tests: you only pay the DB cost where it matters.
  • No custom CLI needed: the helper is regular TypeScript code used by Jest specs.
  • Avoids over-mocking: you can test queries, relations, constraints, migrations… the real stuff.

Current trade-offs

This is explicitly a PoC, and it has important caveats:

  1. No isolation by default The DB content is kept between tests and between runs. That’s fast, but it can create inter-test coupling unless you manage cleanup carefully.
  2. No parallel-worker safety If you run tests with multiple workers, they may collide on the same DB.
  3. Migration runtime Running migrations on test start is great, but can be slow as the migration history grows.
  4. State drift risk “Kept DB” can hide bugs if your schema/data evolves but the DB wasn’t reset as you expected.

What I would implement

If I were to evolve this from PoC to “real project quality”, I’d add:

  • Isolation strategy: truncate tables between tests/suites
  • Parallel test support: per-worker DB name (e.g. _test_w1, _test_w2).
  • Seeding/factories: a standard place for fixtures, factories, and deterministic seeds.

Closing

Django made “real DB tests” feel boring—in a good way. In NestJS + MikroORM, I had to assemble the story myself. This helper is small, explicit, and already valuable for integration tests where real queries matter.

PoC source: PR #1 in abel-castro/blog-nest .

1. Looking back

End 2023 I introduced the first version Sports Dashboard, a minimal web app that tracks the latest results and league tables for Europe’s top football competitions. If you missed that origin story, catch up here.

That version used Angular 16 and a simple Django/REST API. It worked, but I quickly stopped enjoying working with Angular. So I decided to rebuild the project using tools I actually want to use.

2. Why rebuild?

Main reasons:

  • I wanted to stop using Angular.
  • I was learning Next.js 15 and wanted to go deeper.
  • I wanted to own the full stack of a GraphQL API.
  • I wanted to stop self-hosting and intentionally accept vendor lock-in with Vercel.

3. Wins & learnings

GraphQL turned out to be a great match for football data. One query can fetch standings, match results and team info in a single request—no overfetching.

4. The new version

Here the new URL: http://sports-dashboard.abelcastro.dev. The code is also publicly available here.