diff --git a/README.md b/README.md index 5a83be3..5d29c45 100644 --- a/README.md +++ b/README.md @@ -2,135 +2,83 @@ **Write isolated database integration tests with a consistent API across supported test runners and database clients.** -Configure the database and runner once. Add `@DataIntegrationTest` or -`declareDataIntegrationTest()` to a test file. Its fixtures, tests, and teardown then use a -transaction-scoped client whose writes roll back after every test attempt. +Configure your database and test runner once. Each test file then needs one declaration and +access to the shared context. Use the client from that context for every database operation you +want rolled back: -## Recommended pairing: Testcontainers Integration + Data Integration - -**Real infrastructure, isolated test data: two tools that work hand in hand.** +```ts +import { declareDataIntegrationTest } from '@integration-testing/data'; +import { dataContext } from './data-context.js'; -Use **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** -together with this library for repeatable integration tests against a real PostgreSQL instance. -Both packages belong to the `@integration-testing` family and solve complementary parts of the setup. -Explore the [Testcontainers Integration repository](https://github.com/RolandSall/testcontainers-integration) -for its infrastructure APIs and the [published npm package](https://www.npmjs.com/package/@integration-testing/testcontainers) -for installation: - -- **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** - starts disposable infrastructure, provides typed connection resources with mapped ports, and - stops the containers and network after the run. Use its published package to keep Docker - provisioning out of individual tests. -- **@integration-testing/data** connects your database client to that infrastructure, makes a - transaction-scoped client available to fixtures and repositories, and rolls back each test's - writes under Jest or Vitest. +declareDataIntegrationTest(); -```text -@integration-testing/testcontainers - Start PostgreSQL once and supply its connection URL - Application migration command - Apply the same versioned migrations used to deploy the application, once - Jest or Vitest workers - @integration-testing/data - BEGIN → beforeEach → test → afterEach → ROLLBACK - Stop PostgreSQL after all workers finish +test('creates a product', async () => { + const client = dataContext.getCurrentContext().client; + await client.query('INSERT INTO products (sku, stock) VALUES ($1, $2)', ['book', 10]); + const result = await client.query('SELECT stock FROM products WHERE sku = $1', ['book']); + expect(result.rows[0].stock).toBe(10); +}); // The insert is rolled back, including when the assertion fails. ``` -Infrastructure lifecycle and transaction isolation remain separate. You can use the two -libraries together, use this library with an existing database, or use Testcontainers Integration -without this library for tests that need different data-cleanup behavior. - -**Try the complete [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/nestjs-inventory)** -to see the pairing with Prisma, pg, and TypeORM, under both Jest and Vitest: - -```sh -git clone https://github.com/RolandSall/data-integration-testing.git -cd data-integration-testing -git checkout codex/cross-runner-data-tests -bun install --frozen-lockfile -bun run build -bun run generate -bun run typecheck -bun run --cwd examples/nestjs-inventory test -``` +This is the test-file API after setup, using pg and runner globals. The quick start below supplies +the context and runner configuration. Prefer annotations? `@DataIntegrationTest` on one marker +class has the same effect as `declareDataIntegrationTest()`; use one form per file. -The example uses the published Testcontainers Integration package and the local compiled Data -Integration package. `bun run test:consumer` also copies it outside the workspace and installs the -packed data library to verify real consumer behavior without workspace links or source aliases. -Its default run applies application migrations before workers start and verifies rollback through -independent root connections. Docker is required for its PostgreSQL container runs. +## What it does and does not do -### Compatibility between the two packages +Use this library for **direct database and repository integration tests**: queries, constraints, +and multi-write application operations that can run through the supplied transaction client. -The examples pin **`@integration-testing/testcontainers@0.1.0`**. The connection URL is the -boundary between the libraries: Testcontainers Integration supplies a real database endpoint; -Data Integration creates the configured driver and controls transactions through its adapter. -Neither library needs to import the other's source, and Data Integration does not require -Testcontainers as a dependency. - -| Setup with Testcontainers Integration 0.1.0 | Data Integration compatibility | +| The library handles | Your application supplies | | --- | --- | -| Shared `ContainerRuntime` launcher | Verified with PostgreSQL + pg, Prisma 6.19, and TypeORM 0.3 under Jest and Vitest, using an installed data package archive | -| Shared native `createVitestContainerGlobalSetup` | Verified with pg, global migrations, and transaction-scoped tests | -| Shared native `createJestContainerGlobalSetup` | Verified with pg while retaining `@integration-testing/data/jest/environment` | -| Named `@RequiredContainer` with `isolation: 'shared'` and manual global setup | Demonstrated by the small Vitest example; both files select the same named database | -| Generated annotation/project file setup, including `isolation: 'dedicated'` | Not a supported drop-in pairing in this release; file lifecycle coordination is still needed | -| RabbitMQ, MongoDB, or SQL Server containers | Available infrastructure in Testcontainers Integration; this does not add transactional adapter coverage to Data Integration | +| A transaction for each test attempt, including retries | A reachable test database and its real application migrations | +| The same typed transaction client in `beforeEach`, the test, and `afterEach` | Awaited database operations using that client | +| Rollback after successful and failing tests, with cleanup failures reported | Repository or NestJS provider wiring that passes the transaction client into application code | +| Runner lifecycle integration for Jest and Vitest | One context module and the appropriate runner setup | + +**It does not automatically make all application activity transactional.** It does not patch a +production client, intercept HTTP requests, capture another pool's writes, or roll back messages +and background jobs. It does not run migrations automatically or create your application tables. +`beforeAll` and `afterAll` are outside per-test transactions. Concurrent tests within an activated +file are unsupported; parallel files need deliberate fixture and database ownership choices. +See [the rollback boundaries](#rollback-boundaries) before using it for end-to-end tests. + +You can already write rollback hooks by hand. This package is useful when you want to reuse one +typed context and lifecycle contract across supported clients and runners. For one small suite, +ordinary hooks may be sufficient; the [handwritten comparison](#examples-comparison-and-verification) +uses the same application behavior so you can decide. -**Upgrading from the older Testcontainers beta:** replace the positional -`@RequiredContainer(Container.PostgreSql)` declaration with its named form: +## Recommended pairing: Testcontainers Integration + Data Integration -```ts -import { Container, RequiredContainer } from '@integration-testing/testcontainers'; -import { DataIntegrationTest } from '@integration-testing/data'; +**Real infrastructure, isolated test data: two tools that work hand in hand.** -@RequiredContainer({ - database: { kind: Container.PostgreSql, isolation: 'shared' }, -}) -@DataIntegrationTest -export class InventoryIntegrationTest {} -``` +Use **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** +to start a disposable PostgreSQL instance and provide its connection URL. Apply your application's +real migrations once, then use **@integration-testing/data** to roll back each test's writes. +Testcontainers Integration stops the infrastructure after the run; Data Integration manages the +test transactions and its configured clients. -With discovery-based global setup, select that resource using -`resources.getNamed('database', Container.PostgreSql)`. The two decorators have separate jobs: -`@RequiredContainer` declares infrastructure, while `@DataIntegrationTest` activates the data -runner integration. The complete imports and setup are in the -[small container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers). -The explicit runtime launcher and `requiredContainers` global-setup examples below need only -the data declaration. `@ApplicationIntegrationTest` belongs to Testcontainers Integration's -application lifecycle and is not required for these direct database/repository setups. +The [Testcontainers Integration repository](https://github.com/RolandSall/testcontainers-integration) +documents its infrastructure APIs. The [complete paired setup](#complete-setup-testcontainers-with-pg-and-either-runner) +below adds Docker provisioning to the same context and tests used in the quick start. Both libraries +remain independent: an existing database, Docker Compose, or a CI database service also works. -**Resource handoff in 0.1.0:** `injectedContainerResources()` now reads the active file lifecycle, -so it cannot supply resources during Data Integration's earlier initialization. The manual -global-setup examples below restore the shared resources through the exported Vitest context key -or Jest resource-file environment key. They do not install Testcontainers' generated file setup. +## Start here -**Why dedicated containers need more work:** Testcontainers Integration's generated file setup -starts them in `beforeAll`. Data Integration initializes its file context earlier, in Vitest's -`aroundAll` or Jest's `run_start`, so the dedicated resource is not yet available at that point. -Simply stacking annotations or generated setup files does not establish the necessary startup -and teardown ordering. Use the shared launcher or manual global setup documented here. -A dedicated container is per test file, not per test transaction; it would still need the same -application migrations and explicit transaction-client wiring. +1. [Install the package and your runner](#compatibility-and-installation). +2. [Follow the quick start](#quick-start-with-pg-and-vitest): configure once, declare a test file, and run it. +3. [Use Jest](#configure-jest-once) if that is your runner; keep the same context and tests. +4. [Add Testcontainers Integration](#complete-setup-testcontainers-with-pg-and-either-runner) for a disposable database. + +For another client, replace the pg context with [Prisma](#use-prisma-instead-of-pg) or +[TypeORM](#use-typeorm-instead-of-pg). For no Docker, use [Prisma/SQLite](#alternative-prisma-with-sqlite-without-docker). -## Contents - -- [Compatibility between the two packages](#compatibility-between-the-two-packages) -- [Compatibility and installation](#compatibility-and-installation) -- [Complete setup: Testcontainers with pg and either runner](#complete-setup-testcontainers-with-pg-and-either-runner) -- [Configure Vitest once](#configure-vitest-once) -- [Configure Jest once](#configure-jest-once) -- [Write the same transactional tests](#write-the-same-transactional-tests) -- [Alternative: native Testcontainers global setup](#alternative-native-testcontainers-global-setup) -- [Alternative: an existing PostgreSQL database](#alternative-an-existing-postgresql-database) -- [Use Prisma instead of pg](#use-prisma-instead-of-pg) -- [Use TypeORM instead of pg](#use-typeorm-instead-of-pg) -- [Alternative: Prisma with SQLite, without Docker](#alternative-prisma-with-sqlite-without-docker) -- [Inject the transaction into NestJS repositories](#inject-the-transaction-into-nestjs-repositories) -- [Migrations, parallel files, and resource ownership](#migrations-parallel-files-and-resource-ownership) -- [Lifecycle contract and troubleshooting](#lifecycle-contract-and-troubleshooting) -- [Runner-neutral core and other runners](#runner-neutral-core-and-other-runners) -- [Examples, comparison, and verification](#examples-comparison-and-verification) +Further reading: [NestJS provider wiring](#inject-the-transaction-into-nestjs-repositories), +[native runner global setup](#alternative-native-testcontainers-global-setup), +[Testcontainers compatibility](#compatibility-between-the-two-packages), +[migrations and parallel files](#migrations-parallel-files-and-resource-ownership), +[rollback boundaries](#rollback-boundaries), [API and troubleshooting](#lifecycle-contract-and-troubleshooting), +[other runners](#runner-neutral-core-and-other-runners), and [runnable examples](#examples-comparison-and-verification). ## Compatibility and installation @@ -148,7 +96,8 @@ not establish SQL Server transaction support. published to npm. To use it in another application now, build and pack this repository: ```sh -# From this repository's root: +# From a checkout of this repository (Bun 1.3.5): +bun install --frozen-lockfile bun run build npm pack ./packages/data --pack-destination . ``` @@ -160,16 +109,14 @@ npm install --save-dev ./vendor/integration-testing-data-0.1.0-beta.0.tgz ``` After publication, the installation will be `npm install --save-dev @integration-testing/data@beta`. -The Testcontainers Integration package is already published. The examples and independent consumer -pin its tested stable `0.1.0` release for reproducibility: +For the pg quick start, install the driver and TypeScript tooling: ```sh -npm install --save-dev @integration-testing/testcontainers@0.1.0 npm install pg@8 npm install --save-dev typescript@5.9.3 @types/node@22 @types/pg@8 ``` -Choose a runner, or install both to run the same scenarios under both: +Install the runner you use. The quick start uses Vitest; the Jest section provides the alternative: ```sh # Vitest @@ -182,99 +129,19 @@ npm install --save-dev jest@~30.4.2 jest-environment-node@~30.4.1 ts-jest@^29.4. Runner and driver dependencies are optional peers, confined to their corresponding entrypoints. Core, database adapters, and Jest integration support ESM and CommonJS. Vitest integration is ESM. -## Complete setup: Testcontainers with pg and either runner - -This walkthrough uses **one launcher for both runners**, with the published -`@integration-testing/testcontainers` runtime API. Run the commands from your application's root. -The runner-specific native global-setup alternatives are shown later. Set `"type": "module"` in -your application's `package.json` for this walkthrough; Jest configuration files remain `.cjs`. - -The application must already have a real migration command named `migrate` in `package.json`. -That command must read `DATABASE_URL`. Use the same migration files and migration tool as your -application deployment, such as your Prisma migration command or TypeORM migration command. -The [demo's migration entrypoint](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/src/migrate.ts) -is a complete example with migration history. Do not create a different schema inside each test. - -The test below assumes those migrations create the inventory application's `products` table, -with `sku` and `stock` columns. Substitute your own application tables and repository operations. - -### 1. Run application migrations against the supplied URL - -```js -// scripts/migrate-test-database.mjs -import { spawn } from 'node:child_process'; - -export async function runApplicationMigrations(databaseUrl) { - await new Promise((resolve, reject) => { - const child = spawn('npm', ['run', 'migrate'], { - stdio: 'inherit', - env: { ...process.env, DATABASE_URL: databaseUrl }, - }); - child.once('error', reject); - child.once('exit', (code) => { - if (code === 0) resolve(); - else reject(new Error(`Application migrations exited with code ${code}`)); - }); - }); -} -``` - -This invokes your application command in a separate process. It does not print connection URLs, -change the schema model, or make migrations part of per-test transactions. - -### 2. Start infrastructure, migrate once, then launch the selected runner - -```js -// scripts/test-integration.mjs -import { spawn } from 'node:child_process'; -import { resolve } from 'node:path'; -import { - Container, - ContainerRuntime, - createDefaultContainerRegistry, -} from '@integration-testing/testcontainers'; -import { runApplicationMigrations } from './migrate-test-database.mjs'; +## Quick start with pg and Vitest -const runner = process.argv[2] ?? 'vitest'; -if (runner !== 'jest' && runner !== 'vitest') { - throw new Error('Choose jest or vitest'); -} -const runtime = new ContainerRuntime(createDefaultContainerRegistry()); -const failures = []; +This path assumes you have a **test PostgreSQL database** and an application migration command +named `migrate` that reads `DATABASE_URL`. The example queries the inventory application's +`products` table with `sku` and `stock` columns; use your real tables and migrations in your app. +To get a database through Docker instead, finish the context, runner, and test setup here, then +use the [Testcontainers launcher](#complete-setup-testcontainers-with-pg-and-either-runner) for the run step. -try { - const resources = await runtime.start([Container.PostgreSql]); - const databaseUrl = resources.get(Container.PostgreSql).connectionUri; - await runApplicationMigrations(databaseUrl); +Set `"type": "module"` in your application's `package.json`. The files below belong in your +application, and commands run from its root. If your runner and TypeScript configuration already +exist, merge the relevant options into them. - const args = runner === 'jest' - ? [resolve('node_modules/jest/bin/jest.js'), '--config', 'jest.integration.config.cjs'] - : [resolve('node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.integration.config.ts']; - - await new Promise((resolveRun, reject) => { - const child = spawn(process.execPath, [...args, ...process.argv.slice(3)], { - stdio: 'inherit', - env: { ...process.env, DATABASE_URL: databaseUrl }, - }); - child.once('error', reject); - child.once('exit', (code) => { - if (code === 0) resolveRun(); - else reject(new Error(`${runner} exited with code ${code}`)); - }); - }); -} catch (error) { - failures.push(error); -} -try { await runtime.stop(); } catch (error) { failures.push(error); } -if (failures.length) throw new AggregateError(failures, 'Integration run failed'); -``` - -Use Node to execute this launcher. Both runners receive the same connection facts before their -workers start. Each worker creates its own client pool; live Docker handles and transaction -objects are not transferred between processes. The container is stopped after the runner exits, -including an ordinary failing test run. Forced process termination is not a JavaScript cleanup guarantee. - -### 3. Export one shared database context +### 1. Configure the database once ```ts // test/data-context.ts @@ -293,16 +160,20 @@ export const dataContext = createDataIntegrationTestContext({ createDatabase: async (url) => url, // Select the already migrated database. createClient: async (url) => new Pool({ connectionString: url }), closeClient: async (pool) => { await pool.end(); }, - dropDatabase: async () => {}, // The launcher owns the container, not this file. + dropDatabase: async () => {}, // This configuration selects a database; it does not own or drop it. transactions: new PgTransactionAdapter(), }); ``` -There is intentionally no `prepareDatabase` migration callback here. Migrations finished before -workers started. This shared context module contains no runner imports and works with either -setup below. Export the context once and import that same instance from your test files. +This context selects a database URL and manages a pool for each activated file. +`createDatabase` does not create a database in this example, and `dropDatabase` deliberately +does nothing because the configuration does not own the database. Run migrations before workers +start, as shown in step 4; no `prepareDatabase` callback is needed. The context has no runner imports. +Export it once and import that same instance from your test files. + + -## Configure Vitest once +### 2. Connect Vitest once ```ts // test/vitest.setup.ts @@ -328,7 +199,10 @@ export default defineConfig({ }); ``` -Enable decorators in the application's root TypeScript configuration. For a minimal ESM project: +
+TypeScript setup for a new project (tsconfig.json) + +Use this root configuration for the examples, including the optional decorator form: ```json { @@ -346,20 +220,116 @@ Enable decorators in the application's root TypeScript configuration. For a mini } ``` -Run the complete container/migration/test lifecycle: +
-```sh -node scripts/test-integration.mjs vitest -``` +The setup file installs the lifecycle hooks once per test-file environment. The installer checks +for a data declaration after collection; files without one remain inactive. Keep default file +isolation enabled and run tests sequentially within each file. The existing `installVitestDataIntegrationTestSupport(configuration)` signature is also supported and returns a context manager. The factory-based form above makes the context reusable across runners. + + +### 3. Declare a file and write tests + +Use ordinary runner tests and hooks. With the runner globals configured above, this file also +runs unchanged under Jest: + +```ts +// test/products.integration.test.ts +import { declareDataIntegrationTest } from '@integration-testing/data'; +import { dataContext } from './data-context.js'; + +declareDataIntegrationTest(); + +beforeEach(async () => { + await dataContext.getCurrentContext().client.query( + 'INSERT INTO products (sku, stock) VALUES ($1, $2)', + ['book', 10], + ); +}); + +test('decrements stock in the current transaction', async () => { + const client = dataContext.getCurrentContext().client; + await client.query('UPDATE products SET stock = stock - 3 WHERE sku = $1', ['book']); + const result = await client.query<{ stock: number }>( + 'SELECT stock FROM products WHERE sku = $1', ['book'], + ); + expect(result.rows[0]?.stock).toBe(7); +}); + +test('starts with a fresh fixture after the previous test', async () => { + const result = await dataContext.getCurrentContext().client.query<{ stock: number }>( + 'SELECT stock FROM products WHERE sku = $1', ['book'], + ); + expect(result.rows[0]?.stock).toBe(10); +}); + +afterEach(() => { + // The transaction context is still accessible in teardown. + expect(dataContext.getCurrentContext().client).toBeDefined(); +}); +``` + +The file marker activates the whole file, including nested `describe` suites. It does not create +a test-class instance or register test methods. Continue using your runner's ordinary test API. +`beforeEach`, test bodies, and `afterEach` share one transaction per attempt; `beforeAll` and +`afterAll` run outside those transactions. + +If you prefer annotations, replace the `declareDataIntegrationTest` import and call with: + +```ts +import { DataIntegrationTest } from '@integration-testing/data'; + +@DataIntegrationTest +export class ProductRepositoryTest {} +``` + +Use exactly one declaration per activated file. Do not use both forms in the same file. Unmarked +files do not activate database resources. The declaration must execute during test-file collection, +before tests run. Tests inside activated files run sequentially; do not use `.concurrent`. + +### 4. Migrate and run + +```sh +export DATABASE_URL='postgresql://test_user:test_password@localhost:5432/inventory_test' +npm run migrate +npx vitest run --config vitest.integration.config.ts +``` + +Both tests should pass: the first sees stock `7`; the second starts from its own fixture with +stock `10`. Their inserted product is rolled back. No `BEGIN`, `ROLLBACK`, or per-test delete +hooks are needed in this test file. Existing committed rows remain unchanged by rollback. +Choose fixture keys that do not conflict with pre-existing data or other parallel tests. + +**For the next test file:** import the same context, add one declaration, and write tests using +`dataContext.getCurrentContext().client`. The database configuration and installer are reused. +Read the client inside a test or its per-test hooks; do not capture it at module scope. + +### What happens when a file runs + +```text +Before workers: start/select database -> run real application migrations once + Activated file: select database -> create root client/pool -> beforeAll + Test attempt: BEGIN -> beforeEach -> test body -> afterEach -> ROLLBACK + Next attempt: a new transaction, including on a retry + After the file: afterAll -> close root client/pool -> configured resource cleanup +After workers: infrastructure owner stops the disposable database, if it created one +``` + +A declaration activates the file; the runner installer connects that declaration to execution. +The database adapter supplies the transaction-bound client. Together they keep awaited operations +in the same transaction through the test's fixtures and teardown. Application code needs to receive +that client, for example through constructor injection; see [NestJS wiring](#inject-the-transaction-into-nestjs-repositories). + ## Configure Jest once Jest needs **both** the installer and the Data Integration Node environment. The installer registers the context inside Jest's test sandbox; the environment connects it to test execution. -The annotation by itself cannot activate this behavior. +The annotation by itself cannot activate this behavior. Keep quick start steps 1 and 3, and use +this section in place of its Vitest setup. The root TypeScript options are shown in the quick +start; select Jest types as described below. ```ts // test/jest.setup.ts @@ -384,7 +354,10 @@ module.exports = { }; ``` -Create `tsconfig.jest.json`: +
+TypeScript setup for Jest (tsconfig.jest.json) + +Create this alongside the quick start's root `tsconfig.json`: ```json { @@ -405,66 +378,128 @@ for standalone TypeScript checking of package exports. If using only Jest, set t relative imports above are mapped to TypeScript application files by Jest; the installed library still resolves through its public package exports. +
+ +After the database is migrated, run the same `test/products.integration.test.ts` file: + ```sh -node scripts/test-integration.mjs jest +npx jest --config jest.integration.config.cjs ``` -## Write the same transactional tests +Keep `DATABASE_URL` exported as in the quick start. For a disposable database, use the launcher +below with `jest` instead. You do not need to install Vitest in a Jest-only project. -With runner globals enabled as above, this file runs unchanged under either runner: +## Complete setup: Testcontainers with pg and either runner -```ts -// test/products.integration.test.ts -import { DataIntegrationTest } from '@integration-testing/data'; -import { dataContext } from './data-context.js'; +Keep the quick start's context, runner setup, and tests. This walkthrough adds **one launcher +for both runners**, with the published +`@integration-testing/testcontainers` runtime API. Run the commands from your application's root. +The runner-specific native global-setup alternatives are shown later. Set `"type": "module"` in +your application's `package.json` for this walkthrough; Jest configuration files remain `.cjs`. -@DataIntegrationTest -export class ProductRepositoryTest {} +Install the published infrastructure package: -beforeEach(async () => { - await dataContext.getCurrentContext().client.query( - 'INSERT INTO products (sku, stock) VALUES ($1, $2)', - ['book', 10], - ); -}); +```sh +npm install --save-dev @integration-testing/testcontainers@0.1.0 +``` -test('decrements stock in the current transaction', async () => { - const client = dataContext.getCurrentContext().client; - await client.query('UPDATE products SET stock = stock - 3 WHERE sku = $1', ['book']); - const result = await client.query<{ stock: number }>( - 'SELECT stock FROM products WHERE sku = $1', ['book'], - ); - expect(result.rows[0]?.stock).toBe(7); -}); +Docker must be running. The application must already have a real migration command named +`migrate` in `package.json`. +That command must read `DATABASE_URL`. Use the same migration files and migration tool as your +application deployment, such as your Prisma migration command or TypeORM migration command. +The [demo's migration entrypoint](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/src/migrate.ts) +is a complete example with migration history. Do not create a different schema inside each test. -test('starts with a fresh fixture after the previous test', async () => { - const result = await dataContext.getCurrentContext().client.query<{ stock: number }>( - 'SELECT stock FROM products WHERE sku = $1', ['book'], - ); - expect(result.rows[0]?.stock).toBe(10); -}); +The quick start tests assume those migrations create the inventory application's `products` table, +with `sku` and `stock` columns. Substitute your own application tables and repository operations. -afterEach(() => { - // The transaction context is still accessible in teardown. - expect(dataContext.getCurrentContext().client).toBeDefined(); -}); +### 1. Run application migrations against the supplied URL + +```js +// scripts/migrate-test-database.mjs +import { spawn } from 'node:child_process'; + +export async function runApplicationMigrations(databaseUrl) { + await new Promise((resolve, reject) => { + const child = spawn('npm', ['run', 'migrate'], { + stdio: 'inherit', + env: { ...process.env, DATABASE_URL: databaseUrl }, + }); + child.once('error', reject); + child.once('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error(`Application migrations exited with code ${code}`)); + }); + }); +} ``` -The file marker activates the whole file, including nested `describe` suites. It does not create -a test-class instance or register test methods. Continue using your runner's ordinary test API. -`beforeEach`, test bodies, and `afterEach` share one transaction per attempt; `beforeAll` and -`afterAll` run outside those transactions. +This invokes your application command in a separate process. It does not print connection URLs, +change the schema model, or make migrations part of per-test transactions. -If you prefer no marker class, replace the decorator and class with: +### 2. Start infrastructure, migrate once, then launch the selected runner -```ts -import { declareDataIntegrationTest } from '@integration-testing/data'; -declareDataIntegrationTest(); +```js +// scripts/test-integration.mjs +import { spawn } from 'node:child_process'; +import { resolve } from 'node:path'; +import { + Container, + ContainerRuntime, + createDefaultContainerRegistry, +} from '@integration-testing/testcontainers'; +import { runApplicationMigrations } from './migrate-test-database.mjs'; + +const runner = process.argv[2] ?? 'vitest'; +if (runner !== 'jest' && runner !== 'vitest') { + throw new Error('Choose jest or vitest'); +} +const runtime = new ContainerRuntime(createDefaultContainerRegistry()); +const failures = []; + +try { + const resources = await runtime.start([Container.PostgreSql]); + const databaseUrl = resources.get(Container.PostgreSql).connectionUri; + await runApplicationMigrations(databaseUrl); + + const args = runner === 'jest' + ? [resolve('node_modules/jest/bin/jest.js'), '--config', 'jest.integration.config.cjs'] + : [resolve('node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.integration.config.ts']; + + await new Promise((resolveRun, reject) => { + const child = spawn(process.execPath, [...args, ...process.argv.slice(3)], { + stdio: 'inherit', + env: { ...process.env, DATABASE_URL: databaseUrl }, + }); + child.once('error', reject); + child.once('exit', (code) => { + if (code === 0) resolveRun(); + else reject(new Error(`${runner} exited with code ${code}`)); + }); + }); +} catch (error) { + failures.push(error); +} +try { await runtime.stop(); } catch (error) { failures.push(error); } +if (failures.length) throw new AggregateError(failures, 'Integration run failed'); ``` -Use exactly one declaration per activated file. Do not use both forms in the same file. Unmarked -files do not activate database resources. The declaration must execute during test-file collection, -before tests run. Tests inside activated files run sequentially; do not use `.concurrent`. +Use Node to execute this launcher. Both runners receive the same connection facts before their +workers start. Each worker creates its own client pool; live Docker handles and transaction +objects are not transferred between processes. The container is stopped after the runner exits, +including an ordinary failing test run. Forced process termination is not a JavaScript cleanup guarantee. + +### 3. Run the paired lifecycle + +```sh +node scripts/test-integration.mjs vitest +# Or, with the Jest configuration: +node scripts/test-integration.mjs jest +``` + +The launcher supplies `DATABASE_URL`, invokes your real migrations once, runs the selected runner, +and stops its container. You do not need to export a database URL for this mode. Your transactional +test files and `test/data-context.ts` stay the same. ## Alternative: native Testcontainers global setup @@ -520,7 +555,7 @@ For TypeScript checking of the shared JavaScript migration helper, add a declara export function runApplicationMigrations(databaseUrl: string): Promise; ``` -The [small container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers) +The [small container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers) also demonstrates annotation discovery: omit `requiredContainers` and use the named `@RequiredContainer({ database: { kind: Container.PostgreSql, isolation: 'shared' } })` alongside `@DataIntegrationTest` on the same marker class. In that setup, global migrations and each file's @@ -592,7 +627,7 @@ The `prepareResources` callback completes migrations before the database is expo ## Alternative: an existing PostgreSQL database For Docker Compose, a CI service container, or an existing disposable test database, keep the -same data context, runner setup, and tests from the launcher walkthrough. Supply `DATABASE_URL`, +same data context, runner setup, and tests from the quick start. Supply `DATABASE_URL`, run your migrations once, and invoke the runner directly: ```sh @@ -618,7 +653,7 @@ before either runner starts. It never drops the supplied database itself. ## Use Prisma instead of pg -Keep the infrastructure launcher, real migration step, runner installer, and file declaration. +Keep your database provisioning, real migration step, runner installer, and file declaration. Replace the pg context with a context using the application's generated Prisma 6.19 client: ```sh @@ -705,7 +740,7 @@ export const dataContext = createDataIntegrationTestContext({ ``` `ProductEntity` and `ReservationEntity` are the application's own entity definitions; the demo -has complete [EntitySchema examples](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/src/typeorm-repository.ts). +has complete [EntitySchema examples](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/src/typeorm-repository.ts). Run your application migrations globally; do not enable schema synchronization or per-file migration execution. Inside a test or fixture, get repositories from the scoped manager: @@ -842,6 +877,82 @@ The example's optional `bun run test:per-file` command (from `examples/nestjs-in file. Each schema runs the **same application migrations**. It is additional coverage, not the default setup and not permission for tests to invent tables that differ from production. +## Compatibility between the two packages + +The examples pin **`@integration-testing/testcontainers@0.1.0`**. The connection URL is the +boundary between the libraries: Testcontainers Integration supplies a real database endpoint; +Data Integration creates the configured driver and controls transactions through its adapter. +Neither library needs to import the other's source, and Data Integration does not require +Testcontainers as a dependency. + +| Setup with Testcontainers Integration 0.1.0 | Data Integration compatibility | +| --- | --- | +| Shared `ContainerRuntime` launcher | Verified with PostgreSQL + pg, Prisma 6.19, and TypeORM 0.3 under Jest and Vitest, using an installed data package archive | +| Shared native `createVitestContainerGlobalSetup` | Verified with pg, global migrations, and transaction-scoped tests | +| Shared native `createJestContainerGlobalSetup` | Verified with pg while retaining `@integration-testing/data/jest/environment` | +| Named `@RequiredContainer` with `isolation: 'shared'` and manual global setup | Demonstrated by the small Vitest example; both files select the same named database | +| Generated annotation/project file setup, including `isolation: 'dedicated'` | Not a supported drop-in pairing in this release; file lifecycle coordination is still needed | +| RabbitMQ, MongoDB, or SQL Server containers | Available infrastructure in Testcontainers Integration; this does not add transactional adapter coverage to Data Integration | + +**Upgrading from the older Testcontainers beta:** replace the positional +`@RequiredContainer(Container.PostgreSql)` declaration with its named form: + +```ts +import { Container, RequiredContainer } from '@integration-testing/testcontainers'; +import { DataIntegrationTest } from '@integration-testing/data'; + +@RequiredContainer({ + database: { kind: Container.PostgreSql, isolation: 'shared' }, +}) +@DataIntegrationTest +export class InventoryIntegrationTest {} +``` + +With discovery-based global setup, select that resource using +`resources.getNamed('database', Container.PostgreSql)`. The two decorators have separate jobs: +`@RequiredContainer` declares infrastructure, while `@DataIntegrationTest` activates the data +runner integration. The complete imports and setup are in the +[small container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers). +The explicit runtime launcher and `requiredContainers` global-setup examples below need only +the data declaration. `@ApplicationIntegrationTest` belongs to Testcontainers Integration's +application lifecycle and is not required for these direct database/repository setups. + +**Resource handoff in 0.1.0:** `injectedContainerResources()` now reads the active file lifecycle, +so it cannot supply resources during Data Integration's earlier initialization. The manual +global-setup examples below restore the shared resources through the exported Vitest context key +or Jest resource-file environment key. They do not install Testcontainers' generated file setup. + +**Why dedicated containers need more work:** Testcontainers Integration's generated file setup +starts them in `beforeAll`. Data Integration initializes its file context earlier, in Vitest's +`aroundAll` or Jest's `run_start`, so the dedicated resource is not yet available at that point. +Simply stacking annotations or generated setup files does not establish the necessary startup +and teardown ordering. Use the shared launcher or manual global setup documented here. +A dedicated container is per test file, not per test transaction; it would still need the same +application migrations and explicit transaction-client wiring. + +## Rollback boundaries + +The rollback guarantee covers **awaited operations using the supplied transaction client**. +Use the following boundaries to decide whether this is the right isolation strategy for a test: + +| Situation | Behavior and what to do | +| --- | --- | +| Direct queries or repository calls using the supplied client | Included in the test transaction, including per-test fixtures and teardown | +| A root client, another pool, or a separately created Prisma client | Outside the transaction; pass the scoped client to the repository instead | +| A NestJS HTTP request or background worker | Not automatically enrolled; an independently configured connection may commit writes | +| Application migrations or shared schema preparation | Application-owned; run once globally before workers for a shared database | +| Explicit commits or database operations that cannot be rolled back | Outside the guarantee; use a separate isolation and cleanup strategy | +| Queue messages, network calls, files, or other external effects | Not undone by database rollback; manage those resources separately | +| Parallel test files sharing a database | Transactions isolate their uncommitted data, but locks, deadlocks, and sequences still matter; avoid conflicting fixtures | +| `.concurrent` or inherited concurrent tests in an activated file | Rejected; run tests sequentially within the file | +| Callback-style `done` tests/hooks | Unsupported; return a promise or use `async`/`await` | +| A test timeout, unawaited work, or forced process termination | Arbitrary JavaScript cannot be forcibly cancelled; a timeout is not proof that application work stopped | +| Cucumber or a custom runner | No shipped adapter; runner-neutral core does not mean automatic runner support | + +Do not retain a scoped client after its test ends. Context access is invalidated when the +transaction finishes. Rollback/cleanup failures are reported as failures, not evidence of isolation; +inspect and resolve them rather than assuming that a failed cleanup reset the database. + ## Lifecycle contract and troubleshooting | API or option | Meaning | @@ -914,6 +1025,25 @@ global `test` or `describe` or import private Circus modules. Vitest uses `aroun ## Examples, comparison, and verification +**Try the complete [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/main/examples/nestjs-inventory)** +to see the pairing with Prisma, pg, and TypeORM, under both Jest and Vitest: + +```sh +git clone https://github.com/RolandSall/data-integration-testing.git +cd data-integration-testing +bun install --frozen-lockfile +bun run build +bun run generate +bun run typecheck +bun run --cwd examples/nestjs-inventory test +``` + +The example uses the published Testcontainers Integration package and the local compiled Data +Integration package. `bun run test:consumer` also copies it outside the workspace and installs the +packed data library to verify real consumer behavior without workspace links or source aliases. +Its default run applies application migrations before workers start and verifies rollback through +independent root connections. Docker is required for its PostgreSQL container runs. + All examples live in this repository under `examples/`. They are **not shipped to npm**. Only `packages/data` is published, with an allowlist of compiled outputs, declarations, README, and license. Example applications, migrations, tests, and their dependencies remain repository-only. @@ -922,10 +1052,10 @@ archive, preserving independent consumer verification without a separate reposit | Example | What it demonstrates | | --- | --- | -| [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/nestjs-inventory) | Published Testcontainers Integration, installed data artifact, all supported clients/runners, application migrations, independent rollback checks | -| [Demo application provider wiring](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/test/application.ts) | Explicit transaction-client injection without testing imports in application source | -| [Handwritten pg baseline](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/test/baseline.test.ts) | The same application scenarios with ordinary Jest/Vitest hooks | -| [Small pg/container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers) | Combined annotations and native Vitest global setup | +| [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/main/examples/nestjs-inventory) | Published Testcontainers Integration, installed data artifact, all supported clients/runners, application migrations, independent rollback checks | +| [Demo application provider wiring](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/test/application.ts) | Explicit transaction-client injection without testing imports in application source | +| [Handwritten pg baseline](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/test/baseline.test.ts) | The same application scenarios with ordinary Jest/Vitest hooks | +| [Small pg/container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers) | Combined annotations and native Vitest global setup | | [Small Prisma/SQLite example](https://github.com/RolandSall/data-integration-testing/tree/4a1805f20f60ebc8fd08d6156a682d3ca400caf0/examples/without-testcontainers) | Minimal adapter wiring; its tiny schema fixture is not the production-migration walkthrough | Ordinary hooks can implement transaction rollback. This library provides a consistent typed diff --git a/packages/data/README.md b/packages/data/README.md index 5a83be3..5d29c45 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -2,135 +2,83 @@ **Write isolated database integration tests with a consistent API across supported test runners and database clients.** -Configure the database and runner once. Add `@DataIntegrationTest` or -`declareDataIntegrationTest()` to a test file. Its fixtures, tests, and teardown then use a -transaction-scoped client whose writes roll back after every test attempt. +Configure your database and test runner once. Each test file then needs one declaration and +access to the shared context. Use the client from that context for every database operation you +want rolled back: -## Recommended pairing: Testcontainers Integration + Data Integration - -**Real infrastructure, isolated test data: two tools that work hand in hand.** +```ts +import { declareDataIntegrationTest } from '@integration-testing/data'; +import { dataContext } from './data-context.js'; -Use **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** -together with this library for repeatable integration tests against a real PostgreSQL instance. -Both packages belong to the `@integration-testing` family and solve complementary parts of the setup. -Explore the [Testcontainers Integration repository](https://github.com/RolandSall/testcontainers-integration) -for its infrastructure APIs and the [published npm package](https://www.npmjs.com/package/@integration-testing/testcontainers) -for installation: - -- **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** - starts disposable infrastructure, provides typed connection resources with mapped ports, and - stops the containers and network after the run. Use its published package to keep Docker - provisioning out of individual tests. -- **@integration-testing/data** connects your database client to that infrastructure, makes a - transaction-scoped client available to fixtures and repositories, and rolls back each test's - writes under Jest or Vitest. +declareDataIntegrationTest(); -```text -@integration-testing/testcontainers - Start PostgreSQL once and supply its connection URL - Application migration command - Apply the same versioned migrations used to deploy the application, once - Jest or Vitest workers - @integration-testing/data - BEGIN → beforeEach → test → afterEach → ROLLBACK - Stop PostgreSQL after all workers finish +test('creates a product', async () => { + const client = dataContext.getCurrentContext().client; + await client.query('INSERT INTO products (sku, stock) VALUES ($1, $2)', ['book', 10]); + const result = await client.query('SELECT stock FROM products WHERE sku = $1', ['book']); + expect(result.rows[0].stock).toBe(10); +}); // The insert is rolled back, including when the assertion fails. ``` -Infrastructure lifecycle and transaction isolation remain separate. You can use the two -libraries together, use this library with an existing database, or use Testcontainers Integration -without this library for tests that need different data-cleanup behavior. - -**Try the complete [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/nestjs-inventory)** -to see the pairing with Prisma, pg, and TypeORM, under both Jest and Vitest: - -```sh -git clone https://github.com/RolandSall/data-integration-testing.git -cd data-integration-testing -git checkout codex/cross-runner-data-tests -bun install --frozen-lockfile -bun run build -bun run generate -bun run typecheck -bun run --cwd examples/nestjs-inventory test -``` +This is the test-file API after setup, using pg and runner globals. The quick start below supplies +the context and runner configuration. Prefer annotations? `@DataIntegrationTest` on one marker +class has the same effect as `declareDataIntegrationTest()`; use one form per file. -The example uses the published Testcontainers Integration package and the local compiled Data -Integration package. `bun run test:consumer` also copies it outside the workspace and installs the -packed data library to verify real consumer behavior without workspace links or source aliases. -Its default run applies application migrations before workers start and verifies rollback through -independent root connections. Docker is required for its PostgreSQL container runs. +## What it does and does not do -### Compatibility between the two packages +Use this library for **direct database and repository integration tests**: queries, constraints, +and multi-write application operations that can run through the supplied transaction client. -The examples pin **`@integration-testing/testcontainers@0.1.0`**. The connection URL is the -boundary between the libraries: Testcontainers Integration supplies a real database endpoint; -Data Integration creates the configured driver and controls transactions through its adapter. -Neither library needs to import the other's source, and Data Integration does not require -Testcontainers as a dependency. - -| Setup with Testcontainers Integration 0.1.0 | Data Integration compatibility | +| The library handles | Your application supplies | | --- | --- | -| Shared `ContainerRuntime` launcher | Verified with PostgreSQL + pg, Prisma 6.19, and TypeORM 0.3 under Jest and Vitest, using an installed data package archive | -| Shared native `createVitestContainerGlobalSetup` | Verified with pg, global migrations, and transaction-scoped tests | -| Shared native `createJestContainerGlobalSetup` | Verified with pg while retaining `@integration-testing/data/jest/environment` | -| Named `@RequiredContainer` with `isolation: 'shared'` and manual global setup | Demonstrated by the small Vitest example; both files select the same named database | -| Generated annotation/project file setup, including `isolation: 'dedicated'` | Not a supported drop-in pairing in this release; file lifecycle coordination is still needed | -| RabbitMQ, MongoDB, or SQL Server containers | Available infrastructure in Testcontainers Integration; this does not add transactional adapter coverage to Data Integration | +| A transaction for each test attempt, including retries | A reachable test database and its real application migrations | +| The same typed transaction client in `beforeEach`, the test, and `afterEach` | Awaited database operations using that client | +| Rollback after successful and failing tests, with cleanup failures reported | Repository or NestJS provider wiring that passes the transaction client into application code | +| Runner lifecycle integration for Jest and Vitest | One context module and the appropriate runner setup | + +**It does not automatically make all application activity transactional.** It does not patch a +production client, intercept HTTP requests, capture another pool's writes, or roll back messages +and background jobs. It does not run migrations automatically or create your application tables. +`beforeAll` and `afterAll` are outside per-test transactions. Concurrent tests within an activated +file are unsupported; parallel files need deliberate fixture and database ownership choices. +See [the rollback boundaries](#rollback-boundaries) before using it for end-to-end tests. + +You can already write rollback hooks by hand. This package is useful when you want to reuse one +typed context and lifecycle contract across supported clients and runners. For one small suite, +ordinary hooks may be sufficient; the [handwritten comparison](#examples-comparison-and-verification) +uses the same application behavior so you can decide. -**Upgrading from the older Testcontainers beta:** replace the positional -`@RequiredContainer(Container.PostgreSql)` declaration with its named form: +## Recommended pairing: Testcontainers Integration + Data Integration -```ts -import { Container, RequiredContainer } from '@integration-testing/testcontainers'; -import { DataIntegrationTest } from '@integration-testing/data'; +**Real infrastructure, isolated test data: two tools that work hand in hand.** -@RequiredContainer({ - database: { kind: Container.PostgreSql, isolation: 'shared' }, -}) -@DataIntegrationTest -export class InventoryIntegrationTest {} -``` +Use **[@integration-testing/testcontainers](https://www.npmjs.com/package/@integration-testing/testcontainers)** +to start a disposable PostgreSQL instance and provide its connection URL. Apply your application's +real migrations once, then use **@integration-testing/data** to roll back each test's writes. +Testcontainers Integration stops the infrastructure after the run; Data Integration manages the +test transactions and its configured clients. -With discovery-based global setup, select that resource using -`resources.getNamed('database', Container.PostgreSql)`. The two decorators have separate jobs: -`@RequiredContainer` declares infrastructure, while `@DataIntegrationTest` activates the data -runner integration. The complete imports and setup are in the -[small container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers). -The explicit runtime launcher and `requiredContainers` global-setup examples below need only -the data declaration. `@ApplicationIntegrationTest` belongs to Testcontainers Integration's -application lifecycle and is not required for these direct database/repository setups. +The [Testcontainers Integration repository](https://github.com/RolandSall/testcontainers-integration) +documents its infrastructure APIs. The [complete paired setup](#complete-setup-testcontainers-with-pg-and-either-runner) +below adds Docker provisioning to the same context and tests used in the quick start. Both libraries +remain independent: an existing database, Docker Compose, or a CI database service also works. -**Resource handoff in 0.1.0:** `injectedContainerResources()` now reads the active file lifecycle, -so it cannot supply resources during Data Integration's earlier initialization. The manual -global-setup examples below restore the shared resources through the exported Vitest context key -or Jest resource-file environment key. They do not install Testcontainers' generated file setup. +## Start here -**Why dedicated containers need more work:** Testcontainers Integration's generated file setup -starts them in `beforeAll`. Data Integration initializes its file context earlier, in Vitest's -`aroundAll` or Jest's `run_start`, so the dedicated resource is not yet available at that point. -Simply stacking annotations or generated setup files does not establish the necessary startup -and teardown ordering. Use the shared launcher or manual global setup documented here. -A dedicated container is per test file, not per test transaction; it would still need the same -application migrations and explicit transaction-client wiring. +1. [Install the package and your runner](#compatibility-and-installation). +2. [Follow the quick start](#quick-start-with-pg-and-vitest): configure once, declare a test file, and run it. +3. [Use Jest](#configure-jest-once) if that is your runner; keep the same context and tests. +4. [Add Testcontainers Integration](#complete-setup-testcontainers-with-pg-and-either-runner) for a disposable database. + +For another client, replace the pg context with [Prisma](#use-prisma-instead-of-pg) or +[TypeORM](#use-typeorm-instead-of-pg). For no Docker, use [Prisma/SQLite](#alternative-prisma-with-sqlite-without-docker). -## Contents - -- [Compatibility between the two packages](#compatibility-between-the-two-packages) -- [Compatibility and installation](#compatibility-and-installation) -- [Complete setup: Testcontainers with pg and either runner](#complete-setup-testcontainers-with-pg-and-either-runner) -- [Configure Vitest once](#configure-vitest-once) -- [Configure Jest once](#configure-jest-once) -- [Write the same transactional tests](#write-the-same-transactional-tests) -- [Alternative: native Testcontainers global setup](#alternative-native-testcontainers-global-setup) -- [Alternative: an existing PostgreSQL database](#alternative-an-existing-postgresql-database) -- [Use Prisma instead of pg](#use-prisma-instead-of-pg) -- [Use TypeORM instead of pg](#use-typeorm-instead-of-pg) -- [Alternative: Prisma with SQLite, without Docker](#alternative-prisma-with-sqlite-without-docker) -- [Inject the transaction into NestJS repositories](#inject-the-transaction-into-nestjs-repositories) -- [Migrations, parallel files, and resource ownership](#migrations-parallel-files-and-resource-ownership) -- [Lifecycle contract and troubleshooting](#lifecycle-contract-and-troubleshooting) -- [Runner-neutral core and other runners](#runner-neutral-core-and-other-runners) -- [Examples, comparison, and verification](#examples-comparison-and-verification) +Further reading: [NestJS provider wiring](#inject-the-transaction-into-nestjs-repositories), +[native runner global setup](#alternative-native-testcontainers-global-setup), +[Testcontainers compatibility](#compatibility-between-the-two-packages), +[migrations and parallel files](#migrations-parallel-files-and-resource-ownership), +[rollback boundaries](#rollback-boundaries), [API and troubleshooting](#lifecycle-contract-and-troubleshooting), +[other runners](#runner-neutral-core-and-other-runners), and [runnable examples](#examples-comparison-and-verification). ## Compatibility and installation @@ -148,7 +96,8 @@ not establish SQL Server transaction support. published to npm. To use it in another application now, build and pack this repository: ```sh -# From this repository's root: +# From a checkout of this repository (Bun 1.3.5): +bun install --frozen-lockfile bun run build npm pack ./packages/data --pack-destination . ``` @@ -160,16 +109,14 @@ npm install --save-dev ./vendor/integration-testing-data-0.1.0-beta.0.tgz ``` After publication, the installation will be `npm install --save-dev @integration-testing/data@beta`. -The Testcontainers Integration package is already published. The examples and independent consumer -pin its tested stable `0.1.0` release for reproducibility: +For the pg quick start, install the driver and TypeScript tooling: ```sh -npm install --save-dev @integration-testing/testcontainers@0.1.0 npm install pg@8 npm install --save-dev typescript@5.9.3 @types/node@22 @types/pg@8 ``` -Choose a runner, or install both to run the same scenarios under both: +Install the runner you use. The quick start uses Vitest; the Jest section provides the alternative: ```sh # Vitest @@ -182,99 +129,19 @@ npm install --save-dev jest@~30.4.2 jest-environment-node@~30.4.1 ts-jest@^29.4. Runner and driver dependencies are optional peers, confined to their corresponding entrypoints. Core, database adapters, and Jest integration support ESM and CommonJS. Vitest integration is ESM. -## Complete setup: Testcontainers with pg and either runner - -This walkthrough uses **one launcher for both runners**, with the published -`@integration-testing/testcontainers` runtime API. Run the commands from your application's root. -The runner-specific native global-setup alternatives are shown later. Set `"type": "module"` in -your application's `package.json` for this walkthrough; Jest configuration files remain `.cjs`. - -The application must already have a real migration command named `migrate` in `package.json`. -That command must read `DATABASE_URL`. Use the same migration files and migration tool as your -application deployment, such as your Prisma migration command or TypeORM migration command. -The [demo's migration entrypoint](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/src/migrate.ts) -is a complete example with migration history. Do not create a different schema inside each test. - -The test below assumes those migrations create the inventory application's `products` table, -with `sku` and `stock` columns. Substitute your own application tables and repository operations. - -### 1. Run application migrations against the supplied URL - -```js -// scripts/migrate-test-database.mjs -import { spawn } from 'node:child_process'; - -export async function runApplicationMigrations(databaseUrl) { - await new Promise((resolve, reject) => { - const child = spawn('npm', ['run', 'migrate'], { - stdio: 'inherit', - env: { ...process.env, DATABASE_URL: databaseUrl }, - }); - child.once('error', reject); - child.once('exit', (code) => { - if (code === 0) resolve(); - else reject(new Error(`Application migrations exited with code ${code}`)); - }); - }); -} -``` - -This invokes your application command in a separate process. It does not print connection URLs, -change the schema model, or make migrations part of per-test transactions. - -### 2. Start infrastructure, migrate once, then launch the selected runner - -```js -// scripts/test-integration.mjs -import { spawn } from 'node:child_process'; -import { resolve } from 'node:path'; -import { - Container, - ContainerRuntime, - createDefaultContainerRegistry, -} from '@integration-testing/testcontainers'; -import { runApplicationMigrations } from './migrate-test-database.mjs'; +## Quick start with pg and Vitest -const runner = process.argv[2] ?? 'vitest'; -if (runner !== 'jest' && runner !== 'vitest') { - throw new Error('Choose jest or vitest'); -} -const runtime = new ContainerRuntime(createDefaultContainerRegistry()); -const failures = []; +This path assumes you have a **test PostgreSQL database** and an application migration command +named `migrate` that reads `DATABASE_URL`. The example queries the inventory application's +`products` table with `sku` and `stock` columns; use your real tables and migrations in your app. +To get a database through Docker instead, finish the context, runner, and test setup here, then +use the [Testcontainers launcher](#complete-setup-testcontainers-with-pg-and-either-runner) for the run step. -try { - const resources = await runtime.start([Container.PostgreSql]); - const databaseUrl = resources.get(Container.PostgreSql).connectionUri; - await runApplicationMigrations(databaseUrl); +Set `"type": "module"` in your application's `package.json`. The files below belong in your +application, and commands run from its root. If your runner and TypeScript configuration already +exist, merge the relevant options into them. - const args = runner === 'jest' - ? [resolve('node_modules/jest/bin/jest.js'), '--config', 'jest.integration.config.cjs'] - : [resolve('node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.integration.config.ts']; - - await new Promise((resolveRun, reject) => { - const child = spawn(process.execPath, [...args, ...process.argv.slice(3)], { - stdio: 'inherit', - env: { ...process.env, DATABASE_URL: databaseUrl }, - }); - child.once('error', reject); - child.once('exit', (code) => { - if (code === 0) resolveRun(); - else reject(new Error(`${runner} exited with code ${code}`)); - }); - }); -} catch (error) { - failures.push(error); -} -try { await runtime.stop(); } catch (error) { failures.push(error); } -if (failures.length) throw new AggregateError(failures, 'Integration run failed'); -``` - -Use Node to execute this launcher. Both runners receive the same connection facts before their -workers start. Each worker creates its own client pool; live Docker handles and transaction -objects are not transferred between processes. The container is stopped after the runner exits, -including an ordinary failing test run. Forced process termination is not a JavaScript cleanup guarantee. - -### 3. Export one shared database context +### 1. Configure the database once ```ts // test/data-context.ts @@ -293,16 +160,20 @@ export const dataContext = createDataIntegrationTestContext({ createDatabase: async (url) => url, // Select the already migrated database. createClient: async (url) => new Pool({ connectionString: url }), closeClient: async (pool) => { await pool.end(); }, - dropDatabase: async () => {}, // The launcher owns the container, not this file. + dropDatabase: async () => {}, // This configuration selects a database; it does not own or drop it. transactions: new PgTransactionAdapter(), }); ``` -There is intentionally no `prepareDatabase` migration callback here. Migrations finished before -workers started. This shared context module contains no runner imports and works with either -setup below. Export the context once and import that same instance from your test files. +This context selects a database URL and manages a pool for each activated file. +`createDatabase` does not create a database in this example, and `dropDatabase` deliberately +does nothing because the configuration does not own the database. Run migrations before workers +start, as shown in step 4; no `prepareDatabase` callback is needed. The context has no runner imports. +Export it once and import that same instance from your test files. + + -## Configure Vitest once +### 2. Connect Vitest once ```ts // test/vitest.setup.ts @@ -328,7 +199,10 @@ export default defineConfig({ }); ``` -Enable decorators in the application's root TypeScript configuration. For a minimal ESM project: +
+TypeScript setup for a new project (tsconfig.json) + +Use this root configuration for the examples, including the optional decorator form: ```json { @@ -346,20 +220,116 @@ Enable decorators in the application's root TypeScript configuration. For a mini } ``` -Run the complete container/migration/test lifecycle: +
-```sh -node scripts/test-integration.mjs vitest -``` +The setup file installs the lifecycle hooks once per test-file environment. The installer checks +for a data declaration after collection; files without one remain inactive. Keep default file +isolation enabled and run tests sequentially within each file. The existing `installVitestDataIntegrationTestSupport(configuration)` signature is also supported and returns a context manager. The factory-based form above makes the context reusable across runners. + + +### 3. Declare a file and write tests + +Use ordinary runner tests and hooks. With the runner globals configured above, this file also +runs unchanged under Jest: + +```ts +// test/products.integration.test.ts +import { declareDataIntegrationTest } from '@integration-testing/data'; +import { dataContext } from './data-context.js'; + +declareDataIntegrationTest(); + +beforeEach(async () => { + await dataContext.getCurrentContext().client.query( + 'INSERT INTO products (sku, stock) VALUES ($1, $2)', + ['book', 10], + ); +}); + +test('decrements stock in the current transaction', async () => { + const client = dataContext.getCurrentContext().client; + await client.query('UPDATE products SET stock = stock - 3 WHERE sku = $1', ['book']); + const result = await client.query<{ stock: number }>( + 'SELECT stock FROM products WHERE sku = $1', ['book'], + ); + expect(result.rows[0]?.stock).toBe(7); +}); + +test('starts with a fresh fixture after the previous test', async () => { + const result = await dataContext.getCurrentContext().client.query<{ stock: number }>( + 'SELECT stock FROM products WHERE sku = $1', ['book'], + ); + expect(result.rows[0]?.stock).toBe(10); +}); + +afterEach(() => { + // The transaction context is still accessible in teardown. + expect(dataContext.getCurrentContext().client).toBeDefined(); +}); +``` + +The file marker activates the whole file, including nested `describe` suites. It does not create +a test-class instance or register test methods. Continue using your runner's ordinary test API. +`beforeEach`, test bodies, and `afterEach` share one transaction per attempt; `beforeAll` and +`afterAll` run outside those transactions. + +If you prefer annotations, replace the `declareDataIntegrationTest` import and call with: + +```ts +import { DataIntegrationTest } from '@integration-testing/data'; + +@DataIntegrationTest +export class ProductRepositoryTest {} +``` + +Use exactly one declaration per activated file. Do not use both forms in the same file. Unmarked +files do not activate database resources. The declaration must execute during test-file collection, +before tests run. Tests inside activated files run sequentially; do not use `.concurrent`. + +### 4. Migrate and run + +```sh +export DATABASE_URL='postgresql://test_user:test_password@localhost:5432/inventory_test' +npm run migrate +npx vitest run --config vitest.integration.config.ts +``` + +Both tests should pass: the first sees stock `7`; the second starts from its own fixture with +stock `10`. Their inserted product is rolled back. No `BEGIN`, `ROLLBACK`, or per-test delete +hooks are needed in this test file. Existing committed rows remain unchanged by rollback. +Choose fixture keys that do not conflict with pre-existing data or other parallel tests. + +**For the next test file:** import the same context, add one declaration, and write tests using +`dataContext.getCurrentContext().client`. The database configuration and installer are reused. +Read the client inside a test or its per-test hooks; do not capture it at module scope. + +### What happens when a file runs + +```text +Before workers: start/select database -> run real application migrations once + Activated file: select database -> create root client/pool -> beforeAll + Test attempt: BEGIN -> beforeEach -> test body -> afterEach -> ROLLBACK + Next attempt: a new transaction, including on a retry + After the file: afterAll -> close root client/pool -> configured resource cleanup +After workers: infrastructure owner stops the disposable database, if it created one +``` + +A declaration activates the file; the runner installer connects that declaration to execution. +The database adapter supplies the transaction-bound client. Together they keep awaited operations +in the same transaction through the test's fixtures and teardown. Application code needs to receive +that client, for example through constructor injection; see [NestJS wiring](#inject-the-transaction-into-nestjs-repositories). + ## Configure Jest once Jest needs **both** the installer and the Data Integration Node environment. The installer registers the context inside Jest's test sandbox; the environment connects it to test execution. -The annotation by itself cannot activate this behavior. +The annotation by itself cannot activate this behavior. Keep quick start steps 1 and 3, and use +this section in place of its Vitest setup. The root TypeScript options are shown in the quick +start; select Jest types as described below. ```ts // test/jest.setup.ts @@ -384,7 +354,10 @@ module.exports = { }; ``` -Create `tsconfig.jest.json`: +
+TypeScript setup for Jest (tsconfig.jest.json) + +Create this alongside the quick start's root `tsconfig.json`: ```json { @@ -405,66 +378,128 @@ for standalone TypeScript checking of package exports. If using only Jest, set t relative imports above are mapped to TypeScript application files by Jest; the installed library still resolves through its public package exports. +
+ +After the database is migrated, run the same `test/products.integration.test.ts` file: + ```sh -node scripts/test-integration.mjs jest +npx jest --config jest.integration.config.cjs ``` -## Write the same transactional tests +Keep `DATABASE_URL` exported as in the quick start. For a disposable database, use the launcher +below with `jest` instead. You do not need to install Vitest in a Jest-only project. -With runner globals enabled as above, this file runs unchanged under either runner: +## Complete setup: Testcontainers with pg and either runner -```ts -// test/products.integration.test.ts -import { DataIntegrationTest } from '@integration-testing/data'; -import { dataContext } from './data-context.js'; +Keep the quick start's context, runner setup, and tests. This walkthrough adds **one launcher +for both runners**, with the published +`@integration-testing/testcontainers` runtime API. Run the commands from your application's root. +The runner-specific native global-setup alternatives are shown later. Set `"type": "module"` in +your application's `package.json` for this walkthrough; Jest configuration files remain `.cjs`. -@DataIntegrationTest -export class ProductRepositoryTest {} +Install the published infrastructure package: -beforeEach(async () => { - await dataContext.getCurrentContext().client.query( - 'INSERT INTO products (sku, stock) VALUES ($1, $2)', - ['book', 10], - ); -}); +```sh +npm install --save-dev @integration-testing/testcontainers@0.1.0 +``` -test('decrements stock in the current transaction', async () => { - const client = dataContext.getCurrentContext().client; - await client.query('UPDATE products SET stock = stock - 3 WHERE sku = $1', ['book']); - const result = await client.query<{ stock: number }>( - 'SELECT stock FROM products WHERE sku = $1', ['book'], - ); - expect(result.rows[0]?.stock).toBe(7); -}); +Docker must be running. The application must already have a real migration command named +`migrate` in `package.json`. +That command must read `DATABASE_URL`. Use the same migration files and migration tool as your +application deployment, such as your Prisma migration command or TypeORM migration command. +The [demo's migration entrypoint](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/src/migrate.ts) +is a complete example with migration history. Do not create a different schema inside each test. -test('starts with a fresh fixture after the previous test', async () => { - const result = await dataContext.getCurrentContext().client.query<{ stock: number }>( - 'SELECT stock FROM products WHERE sku = $1', ['book'], - ); - expect(result.rows[0]?.stock).toBe(10); -}); +The quick start tests assume those migrations create the inventory application's `products` table, +with `sku` and `stock` columns. Substitute your own application tables and repository operations. -afterEach(() => { - // The transaction context is still accessible in teardown. - expect(dataContext.getCurrentContext().client).toBeDefined(); -}); +### 1. Run application migrations against the supplied URL + +```js +// scripts/migrate-test-database.mjs +import { spawn } from 'node:child_process'; + +export async function runApplicationMigrations(databaseUrl) { + await new Promise((resolve, reject) => { + const child = spawn('npm', ['run', 'migrate'], { + stdio: 'inherit', + env: { ...process.env, DATABASE_URL: databaseUrl }, + }); + child.once('error', reject); + child.once('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error(`Application migrations exited with code ${code}`)); + }); + }); +} ``` -The file marker activates the whole file, including nested `describe` suites. It does not create -a test-class instance or register test methods. Continue using your runner's ordinary test API. -`beforeEach`, test bodies, and `afterEach` share one transaction per attempt; `beforeAll` and -`afterAll` run outside those transactions. +This invokes your application command in a separate process. It does not print connection URLs, +change the schema model, or make migrations part of per-test transactions. -If you prefer no marker class, replace the decorator and class with: +### 2. Start infrastructure, migrate once, then launch the selected runner -```ts -import { declareDataIntegrationTest } from '@integration-testing/data'; -declareDataIntegrationTest(); +```js +// scripts/test-integration.mjs +import { spawn } from 'node:child_process'; +import { resolve } from 'node:path'; +import { + Container, + ContainerRuntime, + createDefaultContainerRegistry, +} from '@integration-testing/testcontainers'; +import { runApplicationMigrations } from './migrate-test-database.mjs'; + +const runner = process.argv[2] ?? 'vitest'; +if (runner !== 'jest' && runner !== 'vitest') { + throw new Error('Choose jest or vitest'); +} +const runtime = new ContainerRuntime(createDefaultContainerRegistry()); +const failures = []; + +try { + const resources = await runtime.start([Container.PostgreSql]); + const databaseUrl = resources.get(Container.PostgreSql).connectionUri; + await runApplicationMigrations(databaseUrl); + + const args = runner === 'jest' + ? [resolve('node_modules/jest/bin/jest.js'), '--config', 'jest.integration.config.cjs'] + : [resolve('node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.integration.config.ts']; + + await new Promise((resolveRun, reject) => { + const child = spawn(process.execPath, [...args, ...process.argv.slice(3)], { + stdio: 'inherit', + env: { ...process.env, DATABASE_URL: databaseUrl }, + }); + child.once('error', reject); + child.once('exit', (code) => { + if (code === 0) resolveRun(); + else reject(new Error(`${runner} exited with code ${code}`)); + }); + }); +} catch (error) { + failures.push(error); +} +try { await runtime.stop(); } catch (error) { failures.push(error); } +if (failures.length) throw new AggregateError(failures, 'Integration run failed'); ``` -Use exactly one declaration per activated file. Do not use both forms in the same file. Unmarked -files do not activate database resources. The declaration must execute during test-file collection, -before tests run. Tests inside activated files run sequentially; do not use `.concurrent`. +Use Node to execute this launcher. Both runners receive the same connection facts before their +workers start. Each worker creates its own client pool; live Docker handles and transaction +objects are not transferred between processes. The container is stopped after the runner exits, +including an ordinary failing test run. Forced process termination is not a JavaScript cleanup guarantee. + +### 3. Run the paired lifecycle + +```sh +node scripts/test-integration.mjs vitest +# Or, with the Jest configuration: +node scripts/test-integration.mjs jest +``` + +The launcher supplies `DATABASE_URL`, invokes your real migrations once, runs the selected runner, +and stops its container. You do not need to export a database URL for this mode. Your transactional +test files and `test/data-context.ts` stay the same. ## Alternative: native Testcontainers global setup @@ -520,7 +555,7 @@ For TypeScript checking of the shared JavaScript migration helper, add a declara export function runApplicationMigrations(databaseUrl: string): Promise; ``` -The [small container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers) +The [small container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers) also demonstrates annotation discovery: omit `requiredContainers` and use the named `@RequiredContainer({ database: { kind: Container.PostgreSql, isolation: 'shared' } })` alongside `@DataIntegrationTest` on the same marker class. In that setup, global migrations and each file's @@ -592,7 +627,7 @@ The `prepareResources` callback completes migrations before the database is expo ## Alternative: an existing PostgreSQL database For Docker Compose, a CI service container, or an existing disposable test database, keep the -same data context, runner setup, and tests from the launcher walkthrough. Supply `DATABASE_URL`, +same data context, runner setup, and tests from the quick start. Supply `DATABASE_URL`, run your migrations once, and invoke the runner directly: ```sh @@ -618,7 +653,7 @@ before either runner starts. It never drops the supplied database itself. ## Use Prisma instead of pg -Keep the infrastructure launcher, real migration step, runner installer, and file declaration. +Keep your database provisioning, real migration step, runner installer, and file declaration. Replace the pg context with a context using the application's generated Prisma 6.19 client: ```sh @@ -705,7 +740,7 @@ export const dataContext = createDataIntegrationTestContext({ ``` `ProductEntity` and `ReservationEntity` are the application's own entity definitions; the demo -has complete [EntitySchema examples](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/src/typeorm-repository.ts). +has complete [EntitySchema examples](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/src/typeorm-repository.ts). Run your application migrations globally; do not enable schema synchronization or per-file migration execution. Inside a test or fixture, get repositories from the scoped manager: @@ -842,6 +877,82 @@ The example's optional `bun run test:per-file` command (from `examples/nestjs-in file. Each schema runs the **same application migrations**. It is additional coverage, not the default setup and not permission for tests to invent tables that differ from production. +## Compatibility between the two packages + +The examples pin **`@integration-testing/testcontainers@0.1.0`**. The connection URL is the +boundary between the libraries: Testcontainers Integration supplies a real database endpoint; +Data Integration creates the configured driver and controls transactions through its adapter. +Neither library needs to import the other's source, and Data Integration does not require +Testcontainers as a dependency. + +| Setup with Testcontainers Integration 0.1.0 | Data Integration compatibility | +| --- | --- | +| Shared `ContainerRuntime` launcher | Verified with PostgreSQL + pg, Prisma 6.19, and TypeORM 0.3 under Jest and Vitest, using an installed data package archive | +| Shared native `createVitestContainerGlobalSetup` | Verified with pg, global migrations, and transaction-scoped tests | +| Shared native `createJestContainerGlobalSetup` | Verified with pg while retaining `@integration-testing/data/jest/environment` | +| Named `@RequiredContainer` with `isolation: 'shared'` and manual global setup | Demonstrated by the small Vitest example; both files select the same named database | +| Generated annotation/project file setup, including `isolation: 'dedicated'` | Not a supported drop-in pairing in this release; file lifecycle coordination is still needed | +| RabbitMQ, MongoDB, or SQL Server containers | Available infrastructure in Testcontainers Integration; this does not add transactional adapter coverage to Data Integration | + +**Upgrading from the older Testcontainers beta:** replace the positional +`@RequiredContainer(Container.PostgreSql)` declaration with its named form: + +```ts +import { Container, RequiredContainer } from '@integration-testing/testcontainers'; +import { DataIntegrationTest } from '@integration-testing/data'; + +@RequiredContainer({ + database: { kind: Container.PostgreSql, isolation: 'shared' }, +}) +@DataIntegrationTest +export class InventoryIntegrationTest {} +``` + +With discovery-based global setup, select that resource using +`resources.getNamed('database', Container.PostgreSql)`. The two decorators have separate jobs: +`@RequiredContainer` declares infrastructure, while `@DataIntegrationTest` activates the data +runner integration. The complete imports and setup are in the +[small container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers). +The explicit runtime launcher and `requiredContainers` global-setup examples below need only +the data declaration. `@ApplicationIntegrationTest` belongs to Testcontainers Integration's +application lifecycle and is not required for these direct database/repository setups. + +**Resource handoff in 0.1.0:** `injectedContainerResources()` now reads the active file lifecycle, +so it cannot supply resources during Data Integration's earlier initialization. The manual +global-setup examples below restore the shared resources through the exported Vitest context key +or Jest resource-file environment key. They do not install Testcontainers' generated file setup. + +**Why dedicated containers need more work:** Testcontainers Integration's generated file setup +starts them in `beforeAll`. Data Integration initializes its file context earlier, in Vitest's +`aroundAll` or Jest's `run_start`, so the dedicated resource is not yet available at that point. +Simply stacking annotations or generated setup files does not establish the necessary startup +and teardown ordering. Use the shared launcher or manual global setup documented here. +A dedicated container is per test file, not per test transaction; it would still need the same +application migrations and explicit transaction-client wiring. + +## Rollback boundaries + +The rollback guarantee covers **awaited operations using the supplied transaction client**. +Use the following boundaries to decide whether this is the right isolation strategy for a test: + +| Situation | Behavior and what to do | +| --- | --- | +| Direct queries or repository calls using the supplied client | Included in the test transaction, including per-test fixtures and teardown | +| A root client, another pool, or a separately created Prisma client | Outside the transaction; pass the scoped client to the repository instead | +| A NestJS HTTP request or background worker | Not automatically enrolled; an independently configured connection may commit writes | +| Application migrations or shared schema preparation | Application-owned; run once globally before workers for a shared database | +| Explicit commits or database operations that cannot be rolled back | Outside the guarantee; use a separate isolation and cleanup strategy | +| Queue messages, network calls, files, or other external effects | Not undone by database rollback; manage those resources separately | +| Parallel test files sharing a database | Transactions isolate their uncommitted data, but locks, deadlocks, and sequences still matter; avoid conflicting fixtures | +| `.concurrent` or inherited concurrent tests in an activated file | Rejected; run tests sequentially within the file | +| Callback-style `done` tests/hooks | Unsupported; return a promise or use `async`/`await` | +| A test timeout, unawaited work, or forced process termination | Arbitrary JavaScript cannot be forcibly cancelled; a timeout is not proof that application work stopped | +| Cucumber or a custom runner | No shipped adapter; runner-neutral core does not mean automatic runner support | + +Do not retain a scoped client after its test ends. Context access is invalidated when the +transaction finishes. Rollback/cleanup failures are reported as failures, not evidence of isolation; +inspect and resolve them rather than assuming that a failed cleanup reset the database. + ## Lifecycle contract and troubleshooting | API or option | Meaning | @@ -914,6 +1025,25 @@ global `test` or `describe` or import private Circus modules. Vitest uses `aroun ## Examples, comparison, and verification +**Try the complete [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/main/examples/nestjs-inventory)** +to see the pairing with Prisma, pg, and TypeORM, under both Jest and Vitest: + +```sh +git clone https://github.com/RolandSall/data-integration-testing.git +cd data-integration-testing +bun install --frozen-lockfile +bun run build +bun run generate +bun run typecheck +bun run --cwd examples/nestjs-inventory test +``` + +The example uses the published Testcontainers Integration package and the local compiled Data +Integration package. `bun run test:consumer` also copies it outside the workspace and installs the +packed data library to verify real consumer behavior without workspace links or source aliases. +Its default run applies application migrations before workers start and verifies rollback through +independent root connections. Docker is required for its PostgreSQL container runs. + All examples live in this repository under `examples/`. They are **not shipped to npm**. Only `packages/data` is published, with an allowlist of compiled outputs, declarations, README, and license. Example applications, migrations, tests, and their dependencies remain repository-only. @@ -922,10 +1052,10 @@ archive, preserving independent consumer verification without a separate reposit | Example | What it demonstrates | | --- | --- | -| [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/nestjs-inventory) | Published Testcontainers Integration, installed data artifact, all supported clients/runners, application migrations, independent rollback checks | -| [Demo application provider wiring](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/test/application.ts) | Explicit transaction-client injection without testing imports in application source | -| [Handwritten pg baseline](https://github.com/RolandSall/data-integration-testing/blob/codex/cross-runner-data-tests/examples/nestjs-inventory/test/baseline.test.ts) | The same application scenarios with ordinary Jest/Vitest hooks | -| [Small pg/container example](https://github.com/RolandSall/data-integration-testing/tree/codex/cross-runner-data-tests/examples/with-testcontainers) | Combined annotations and native Vitest global setup | +| [NestJS inventory demo](https://github.com/RolandSall/data-integration-testing/tree/main/examples/nestjs-inventory) | Published Testcontainers Integration, installed data artifact, all supported clients/runners, application migrations, independent rollback checks | +| [Demo application provider wiring](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/test/application.ts) | Explicit transaction-client injection without testing imports in application source | +| [Handwritten pg baseline](https://github.com/RolandSall/data-integration-testing/blob/main/examples/nestjs-inventory/test/baseline.test.ts) | The same application scenarios with ordinary Jest/Vitest hooks | +| [Small pg/container example](https://github.com/RolandSall/data-integration-testing/tree/main/examples/with-testcontainers) | Combined annotations and native Vitest global setup | | [Small Prisma/SQLite example](https://github.com/RolandSall/data-integration-testing/tree/4a1805f20f60ebc8fd08d6156a682d3ca400caf0/examples/without-testcontainers) | Minimal adapter wiring; its tiny schema fixture is not the production-migration walkthrough | Ordinary hooks can implement transaction rollback. This library provides a consistent typed