@dolphjs/testing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The (MIT License)
2
+
3
+ Copyright (c) 2026 dolphtech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @dolphjs/testing
2
+
3
+ Official testing utilities for DolphJS apps. A devDependency, not part of the runtime bundle — nothing here ships to production.
4
+
5
+ ## Why this exists
6
+
7
+ Dolph's DI isn't a request-scoped container like Nest's `TestingModule`. Services are resolved **once**, the moment the module declaring their `@Component` is first imported, and cached forever in a process-wide `GlobalServiceRegistry`. That's a deliberate, simpler design — but it means "mocking a service" is really "winning a race": your mock has to be sitting in the registry *before* the component module is imported for the first time, or the real service has already been built and cached first.
8
+
9
+ Everything in this package exists to make that ordering a non-issue, and to keep tests fast by never paying for more framework machinery than a given test actually needs.
10
+
11
+ ## The three tiers
12
+
13
+ Write specs at the cheapest tier that can prove the behavior. Most of your suite should be tier 1.
14
+
15
+ ### 1. Service unit tests — `*.spec.ts`
16
+
17
+ Services are plain classes. `DolphServiceHandler` adds nothing but a `name` field — no decoration is required to construct one yourself. Don't touch `@dolphjs/testing` at all here; just `new` the service with mocked collaborators.
18
+
19
+ ```ts
20
+ // order.service.spec.ts
21
+ import { OrderService } from './order.service';
22
+
23
+ it('rejects a negative total', async () => {
24
+ const mockRepo = { findTotalById: jest.fn().mockResolvedValue(-5) };
25
+ const service = new OrderService(mockRepo);
26
+
27
+ await expect(service.getTotal('o-1')).rejects.toThrow();
28
+ });
29
+ ```
30
+
31
+ ### 2. Controller unit tests — `*.spec.ts`
32
+
33
+ **Prefer constructor injection over named-property injection on controllers.** `@Component` populates both the same way at boot, but only constructor injection lets a test hand in a mock via plain `new`:
34
+
35
+ ```ts
36
+ // good — testable with `new OrderController(mockService)`
37
+ constructor(private readonly orderService: OrderService) { super(); }
38
+
39
+ // works at runtime, but a unit test can't set this without reaching
40
+ // past the type system: `(controller as any).OrderService = mock`
41
+ private OrderService: OrderService;
42
+ ```
43
+
44
+ Route handlers that `return` their result (rather than writing to `res` directly) can be called like any other method — no Express involved:
45
+
46
+ ```ts
47
+ // order.controller.spec.ts
48
+ import { OrderController } from './order.controller';
49
+
50
+ it('delegates to the service', () => {
51
+ const mockService = { getTotal: jest.fn().mockReturnValue(99) };
52
+ const controller = new OrderController(mockService as any);
53
+
54
+ expect(controller.getTotal('o-1')).toEqual({ total: 99 });
55
+ });
56
+ ```
57
+
58
+ ### 3. Component e2e tests — `*.e2e-spec.ts`
59
+
60
+ For the routes/middleware/param-decoration path itself — real Express, real `@Component`/`@Route` metadata, mocked services underneath. Use `createTestingApp`:
61
+
62
+ ```ts
63
+ // order.component.e2e-spec.ts
64
+ import request from 'supertest';
65
+ import { createTestingApp, TestingApp } from '@dolphjs/testing';
66
+ import { OrderService } from './order.service';
67
+
68
+ describe('OrderComponent', () => {
69
+ let app: TestingApp;
70
+
71
+ beforeAll(async () => {
72
+ app = await createTestingApp({
73
+ components: [() => import('./order.component').then((m) => m.OrderComponent)],
74
+ overrides: [{ service: OrderService, useValue: { getTotal: jest.fn().mockReturnValue(77) } }],
75
+ });
76
+ });
77
+
78
+ afterAll(() => app.close());
79
+
80
+ it('serves through real routing', async () => {
81
+ const res = await request(app.engine).get('/orders/o-1/total');
82
+ expect(res.body.data).toEqual({ total: 77 });
83
+ });
84
+ });
85
+ ```
86
+
87
+ Two details matter here:
88
+
89
+ - **`components` takes a lazy loader, not a static import.** A top-of-file `import { OrderComponent } from './order.component'` runs `@Component` immediately, before `beforeAll` gets a chance to seed any mocks — the real `OrderService` would already be built and cached. `createTestingApp` seeds `overrides` into the registry first, *then* awaits your loader, so the import happens after the mocks are in place.
90
+ - **`app.engine` is a real, already-listening server — not a bare Express app.** `createTestingApp` never calls `DolphFactory#start()`, so it never installs `start()`'s process-level `SIGTERM`/`uncaughtException`/`unhandledRejection` handlers meant for a long-running server; registering those once per spec file is exactly why a `.start()`-based suite needs `jest --forceExit --detectOpenHandles` to exit cleanly. But it does bind one real ephemeral port via `http.createServer(engine).listen(0)`, reused for every request in the spec and closed by `app.close()` — supertest binds a **fresh** ephemeral server on every single call when handed a bare Express app instead of one already listening, which gets expensive fast on anything but a one-or-two-request spec. `examples/` in this repo runs three full tiers, including a real e2e test with several requests, and exits on its own with neither Jest flag — see `npm run test:examples`.
91
+
92
+ ## Jest config gotcha
93
+
94
+ Jest's default `testMatch` picks up `*.spec.ts` and `*.test.ts`, but **not** `*.e2e-spec.ts` — there's no dot before "spec" for the pattern to match. If you use the `*.e2e-spec.ts` convention above, add it explicitly:
95
+
96
+ ```js
97
+ // jest.config.js
98
+ module.exports = {
99
+ preset: 'ts-jest',
100
+ testMatch: ['**/*.spec.ts', '**/*.e2e-spec.ts'],
101
+ };
102
+ ```
103
+
104
+ At enterprise scale — hundreds of spec files — `ts-jest` type-checking every file on every run stops being free. Swap the transform for `@swc/jest` (or `esbuild-jest`) once compile time shows up in CI; the spec files themselves don't change.
105
+
106
+ ## API
107
+
108
+ - **`TestingRegistry.override(ServiceClass, mock)`** — seed a mock directly, for cases `createTestingApp`'s `overrides` doesn't cover.
109
+ - **`TestingRegistry.get(ServiceClass)` / `.has(...)`** — read the current registry state.
110
+ - **`TestingRegistry.reset()`** — clear every cached service. Call between test files, never mid-file.
111
+ - **`createTestingApp({ components, overrides?, middlewares? })`** — see above. Returns `{ engine, get, close }`; `close()` is async — it closes the ephemeral server before resolving, so `afterAll(() => app.close())` is enough (Jest awaits the returned promise).
112
+ - **`createSqliteTestDataSource({ entities })`** — in-memory `better-sqlite3` TypeORM `DataSource`, entity classes passed directly (no glob strings, no `dolph_config.yaml`).
113
+ - **`createSqliteTestSequelize()`** — in-memory `sqlite3` Sequelize instance; call `Model.init({...}, { sequelize })` against it yourself.
114
+ - **`createMongoMemoryTestServer()`** — real ephemeral MongoDB via `mongodb-memory-server`; returns `{ uri, stop }`.
115
+
116
+ The three DB helpers lazily `require()` their driver, so an app that only uses Mongoose never resolves `typeorm`/`sequelize`, and vice versa — install only what you use.
117
+
118
+ ## Development
119
+
120
+ ```
121
+ npm install
122
+ npm run typecheck # against a locally-built @dolphjs/dolph (file:../dolph/dist)
123
+ npm run test:examples # runs examples/ — the three tiers, against the real framework
124
+ npm run build
125
+ ```
126
+
127
+ `@dolphjs/dolph` is linked via `file:../dolph/dist`, not `file:../dolph` — the repo root has no `main`/`types` in its `package.json`, so a link to the repo root falls through to raw `.ts` source instead of the compiled build. Run `npm run build` in `../dolph` first if `dist/` is stale.
@@ -0,0 +1,12 @@
1
+ export interface MongoMemoryTestServer {
2
+ uri: string;
3
+ stop: () => Promise<void>;
4
+ }
5
+ /**
6
+ * Spins up a real, ephemeral MongoDB instance in-process for tests — no
7
+ * mocked driver, no shared dev database. Pass `.uri` to `mongoose.connect()`
8
+ * (or `autoInitMongo({ url: uri })`) in your test setup, and `.stop()` in
9
+ * teardown. Requires `mongodb-memory-server` in the consuming app; lazily
10
+ * required so apps that don't use Mongoose never pay to resolve it.
11
+ */
12
+ export declare function createMongoMemoryTestServer(): Promise<MongoMemoryTestServer>;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMongoMemoryTestServer = createMongoMemoryTestServer;
4
+ /**
5
+ * Spins up a real, ephemeral MongoDB instance in-process for tests — no
6
+ * mocked driver, no shared dev database. Pass `.uri` to `mongoose.connect()`
7
+ * (or `autoInitMongo({ url: uri })`) in your test setup, and `.stop()` in
8
+ * teardown. Requires `mongodb-memory-server` in the consuming app; lazily
9
+ * required so apps that don't use Mongoose never pay to resolve it.
10
+ */
11
+ async function createMongoMemoryTestServer() {
12
+ let MongoMemoryServer;
13
+ try {
14
+ ({ MongoMemoryServer } = require('mongodb-memory-server'));
15
+ }
16
+ catch {
17
+ throw new Error('`mongodb-memory-server` is not installed. Run `npm i -D mongodb-memory-server` to use createMongoMemoryTestServer().');
18
+ }
19
+ const server = await MongoMemoryServer.create();
20
+ return {
21
+ uri: server.getUri(),
22
+ stop: () => server.stop(),
23
+ };
24
+ }
25
+ //# sourceMappingURL=mongo-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongo-memory.js","sourceRoot":"","sources":["../../src/db/mongo-memory.ts"],"names":[],"mappings":";;AAYA,kEAeC;AAtBD;;;;;;GAMG;AACI,KAAK,UAAU,2BAA2B;IAC7C,IAAI,iBAAiD,CAAC;IACtD,IAAI,CAAC;QACD,CAAC,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACL,MAAM,IAAI,KAAK,CACX,sHAAsH,CACzH,CAAC;IACN,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,CAAC;IAChD,OAAO;QACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE;QACpB,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;KAC5B,CAAC;AACN,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Sequelize } from 'sequelize';
2
+ /**
3
+ * An in-memory `sqlite3`-backed Sequelize instance for tests.
4
+ *
5
+ * Hands back a bare, connected instance only — Sequelize models are bound to
6
+ * an instance at `Model.init({...}, { sequelize, ... })`, so re-attaching
7
+ * already-initialized models the way `autoInitSql` does for a real app isn't
8
+ * meaningful here. Call `Model.init` against the returned instance in your
9
+ * test setup. Requires `sequelize` and `sqlite3` in the consuming app;
10
+ * lazily required so apps that don't use Sequelize never pay to resolve them.
11
+ */
12
+ export declare function createSqliteTestSequelize(): Promise<Sequelize>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSqliteTestSequelize = createSqliteTestSequelize;
4
+ /**
5
+ * An in-memory `sqlite3`-backed Sequelize instance for tests.
6
+ *
7
+ * Hands back a bare, connected instance only — Sequelize models are bound to
8
+ * an instance at `Model.init({...}, { sequelize, ... })`, so re-attaching
9
+ * already-initialized models the way `autoInitSql` does for a real app isn't
10
+ * meaningful here. Call `Model.init` against the returned instance in your
11
+ * test setup. Requires `sequelize` and `sqlite3` in the consuming app;
12
+ * lazily required so apps that don't use Sequelize never pay to resolve them.
13
+ */
14
+ async function createSqliteTestSequelize() {
15
+ let SequelizeCtor;
16
+ try {
17
+ ({ Sequelize: SequelizeCtor } = require('sequelize'));
18
+ }
19
+ catch {
20
+ throw new Error('`sequelize` is not installed. Run `npm i -D sequelize sqlite3` to use createSqliteTestSequelize().');
21
+ }
22
+ const sequelize = new SequelizeCtor({
23
+ dialect: 'sqlite',
24
+ storage: ':memory:',
25
+ logging: false,
26
+ });
27
+ await sequelize.authenticate();
28
+ return sequelize;
29
+ }
30
+ //# sourceMappingURL=sqlite-sequelize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-sequelize.js","sourceRoot":"","sources":["../../src/db/sqlite-sequelize.ts"],"names":[],"mappings":";;AAYA,8DAkBC;AA5BD;;;;;;;;;GASG;AACI,KAAK,UAAU,yBAAyB;IAC3C,IAAI,aAA+B,CAAC;IACpC,IAAI,CAAC;QACD,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACL,MAAM,IAAI,KAAK,CACX,oGAAoG,CACvG,CAAC;IACN,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,aAAa,CAAC;QAChC,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE,UAAU;QACnB,OAAO,EAAE,KAAK;KACjB,CAAC,CAAC;IAEH,MAAM,SAAS,CAAC,YAAY,EAAE,CAAC;IAC/B,OAAO,SAAS,CAAC;AACrB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { DataSource } from 'typeorm';
2
+ export interface SqliteTestDataSourceOptions {
3
+ /** Entity classes — passed straight to TypeORM, no glob strings or `dolph_config.yaml` involved. */
4
+ entities: Function[];
5
+ /** Defaults to `true`; the whole point of an in-memory DB is a fresh schema per run. */
6
+ synchronize?: boolean;
7
+ }
8
+ /**
9
+ * An in-memory, `better-sqlite3`-backed TypeORM `DataSource` for tests.
10
+ *
11
+ * Bypasses `autoInitTypeOrm`/`dolph_config.yaml` entirely — those exist to
12
+ * wire a real database at app boot, which is unnecessary ceremony (and a
13
+ * shared on-disk file mutation) for a test that just needs a throwaway
14
+ * schema. Requires `typeorm` and `better-sqlite3` in the consuming app;
15
+ * lazily required so apps that don't use TypeORM never pay to resolve them.
16
+ */
17
+ export declare function createSqliteTestDataSource(options: SqliteTestDataSourceOptions): Promise<DataSource>;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSqliteTestDataSource = createSqliteTestDataSource;
4
+ /**
5
+ * An in-memory, `better-sqlite3`-backed TypeORM `DataSource` for tests.
6
+ *
7
+ * Bypasses `autoInitTypeOrm`/`dolph_config.yaml` entirely — those exist to
8
+ * wire a real database at app boot, which is unnecessary ceremony (and a
9
+ * shared on-disk file mutation) for a test that just needs a throwaway
10
+ * schema. Requires `typeorm` and `better-sqlite3` in the consuming app;
11
+ * lazily required so apps that don't use TypeORM never pay to resolve them.
12
+ */
13
+ async function createSqliteTestDataSource(options) {
14
+ let DataSourceCtor;
15
+ try {
16
+ ({ DataSource: DataSourceCtor } = require('typeorm'));
17
+ }
18
+ catch {
19
+ throw new Error('`typeorm` is not installed. Run `npm i -D typeorm better-sqlite3` to use createSqliteTestDataSource().');
20
+ }
21
+ const dataSource = new DataSourceCtor({
22
+ type: 'better-sqlite3',
23
+ database: ':memory:',
24
+ dropSchema: true,
25
+ entities: options.entities,
26
+ synchronize: options.synchronize ?? true,
27
+ logging: false,
28
+ });
29
+ await dataSource.initialize();
30
+ return dataSource;
31
+ }
32
+ //# sourceMappingURL=sqlite-typeorm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-typeorm.js","sourceRoot":"","sources":["../../src/db/sqlite-typeorm.ts"],"names":[],"mappings":";;AAkBA,gEAqBC;AA9BD;;;;;;;;GAQG;AACI,KAAK,UAAU,0BAA0B,CAAC,OAAoC;IACjF,IAAI,cAAiC,CAAC;IACtC,IAAI,CAAC;QACD,CAAC,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACL,MAAM,IAAI,KAAK,CACX,wGAAwG,CAC3G,CAAC;IACN,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,cAAc,CAAC;QAClC,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,UAAU;QACpB,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;QACxC,OAAO,EAAE,KAAK;KACV,CAAC,CAAC;IAEV,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC;IAC9B,OAAO,UAAU,CAAC;AACtB,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { TestingRegistry } from './testing-registry';
2
+ export type { Ctor } from './testing-registry';
3
+ export { createTestingApp } from './testing-app';
4
+ export type { TestingApp, TestingAppOptions, ComponentRef, ServiceOverride } from './testing-app';
5
+ export { createSqliteTestDataSource } from './db/sqlite-typeorm';
6
+ export type { SqliteTestDataSourceOptions } from './db/sqlite-typeorm';
7
+ export { createSqliteTestSequelize } from './db/sqlite-sequelize';
8
+ export { createMongoMemoryTestServer } from './db/mongo-memory';
9
+ export type { MongoMemoryTestServer } from './db/mongo-memory';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMongoMemoryTestServer = exports.createSqliteTestSequelize = exports.createSqliteTestDataSource = exports.createTestingApp = exports.TestingRegistry = void 0;
4
+ var testing_registry_1 = require("./testing-registry");
5
+ Object.defineProperty(exports, "TestingRegistry", { enumerable: true, get: function () { return testing_registry_1.TestingRegistry; } });
6
+ var testing_app_1 = require("./testing-app");
7
+ Object.defineProperty(exports, "createTestingApp", { enumerable: true, get: function () { return testing_app_1.createTestingApp; } });
8
+ var sqlite_typeorm_1 = require("./db/sqlite-typeorm");
9
+ Object.defineProperty(exports, "createSqliteTestDataSource", { enumerable: true, get: function () { return sqlite_typeorm_1.createSqliteTestDataSource; } });
10
+ var sqlite_sequelize_1 = require("./db/sqlite-sequelize");
11
+ Object.defineProperty(exports, "createSqliteTestSequelize", { enumerable: true, get: function () { return sqlite_sequelize_1.createSqliteTestSequelize; } });
12
+ var mongo_memory_1 = require("./db/mongo-memory");
13
+ Object.defineProperty(exports, "createMongoMemoryTestServer", { enumerable: true, get: function () { return mongo_memory_1.createMongoMemoryTestServer; } });
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uDAAqD;AAA5C,mHAAA,eAAe,OAAA;AAGxB,6CAAiD;AAAxC,+GAAA,gBAAgB,OAAA;AAGzB,sDAAiE;AAAxD,4HAAA,0BAA0B,OAAA;AAGnC,0DAAkE;AAAzD,6HAAA,yBAAyB,OAAA;AAElC,kDAAgE;AAAvD,2HAAA,2BAA2B,OAAA"}
@@ -0,0 +1,51 @@
1
+ import 'reflect-metadata';
2
+ import { Server } from 'http';
3
+ import type { RequestHandler } from 'express';
4
+ import type { Ctor } from './testing-registry';
5
+ /**
6
+ * Either the component class itself (when no override is needed — the module
7
+ * is presumably already imported at the top of the spec file) or a lazy
8
+ * loader (`() => import('../src/components/user/user.component')`) whose
9
+ * import is deferred until after mocks are seeded, so `@Component` resolves
10
+ * against the mocks instead of building the real services first.
11
+ */
12
+ export type ComponentRef = Ctor | (() => Promise<Ctor | {
13
+ default: Ctor;
14
+ }>);
15
+ export interface ServiceOverride<T = any> {
16
+ service: Ctor<T>;
17
+ useValue: T;
18
+ }
19
+ export interface TestingAppOptions {
20
+ components: ComponentRef[];
21
+ /** Mocks seeded into the registry before `components` are resolved. */
22
+ overrides?: ServiceOverride[];
23
+ /** Applied before route registration — same ordering as `DolphFactory`'s constructor. */
24
+ middlewares?: RequestHandler[];
25
+ }
26
+ export interface TestingApp {
27
+ /** A real, already-listening ephemeral server — hand it straight to supertest. */
28
+ engine: Server;
29
+ /** Reads a service instance (real or mocked) straight off the registry, for direct assertions. */
30
+ get<T>(serviceClass: Ctor<T>): T | undefined;
31
+ /** Closes the ephemeral server and clears the process-wide registry. Call in `afterAll`. */
32
+ close(): Promise<void>;
33
+ }
34
+ /**
35
+ * Builds an isolated Dolph app for tests, without ever calling `DolphFactory#start()`.
36
+ *
37
+ * `start()` installs process-level `SIGTERM`/`uncaughtException`/`unhandledRejection`
38
+ * handlers meant for a long-running server — registering those once per spec
39
+ * file leaks across every other file that runs in the same Jest worker,
40
+ * which is exactly why the framework's own suite used to need
41
+ * `--forceExit --detectOpenHandles`.
42
+ *
43
+ * That said, `engine` is a real, listening server, not a bare Express app —
44
+ * supertest binds a brand new ephemeral server on every single request when
45
+ * handed a plain app instead of one already listening, which gets expensive
46
+ * fast across a spec with more than a handful of requests. Binding one
47
+ * ephemeral port here, reused for every request and closed in `close()`,
48
+ * gets the same effect `start()` gives a real app without its process-level
49
+ * side effects.
50
+ */
51
+ export declare function createTestingApp(options: TestingAppOptions): Promise<TestingApp>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTestingApp = createTestingApp;
4
+ require("reflect-metadata");
5
+ const http_1 = require("http");
6
+ const dolph_1 = require("@dolphjs/dolph");
7
+ const isLoader = (ref) => typeof ref === 'function' && ref.prototype === undefined;
8
+ /**
9
+ * Builds an isolated Dolph app for tests, without ever calling `DolphFactory#start()`.
10
+ *
11
+ * `start()` installs process-level `SIGTERM`/`uncaughtException`/`unhandledRejection`
12
+ * handlers meant for a long-running server — registering those once per spec
13
+ * file leaks across every other file that runs in the same Jest worker,
14
+ * which is exactly why the framework's own suite used to need
15
+ * `--forceExit --detectOpenHandles`.
16
+ *
17
+ * That said, `engine` is a real, listening server, not a bare Express app —
18
+ * supertest binds a brand new ephemeral server on every single request when
19
+ * handed a plain app instead of one already listening, which gets expensive
20
+ * fast across a spec with more than a handful of requests. Binding one
21
+ * ephemeral port here, reused for every request and closed in `close()`,
22
+ * gets the same effect `start()` gives a real app without its process-level
23
+ * side effects.
24
+ */
25
+ async function createTestingApp(options) {
26
+ const { components, overrides = [], middlewares } = options;
27
+ // 1. Seed mocks first — see the module-level doc comment on why ordering matters.
28
+ for (const { service, useValue } of overrides) {
29
+ dolph_1.GlobalServiceRegistry.set(service, useValue);
30
+ }
31
+ // 2. Resolve components. Importing a loader here — for the first time in
32
+ // this module registry — is what triggers `@Component`, by which point
33
+ // the mocks above are already in place.
34
+ const resolved = [];
35
+ for (const ref of components) {
36
+ if (isLoader(ref)) {
37
+ const mod = await ref();
38
+ resolved.push('default' in mod ? mod.default : mod);
39
+ }
40
+ else {
41
+ resolved.push(ref);
42
+ }
43
+ }
44
+ const factory = new dolph_1.DolphFactory(resolved, middlewares);
45
+ const server = (0, http_1.createServer)(factory.engine()).listen(0);
46
+ return {
47
+ engine: server,
48
+ get: (serviceClass) => dolph_1.GlobalServiceRegistry.get(serviceClass),
49
+ close: () => new Promise((resolve) => {
50
+ dolph_1.GlobalServiceRegistry._reset();
51
+ server.close(() => resolve());
52
+ }),
53
+ };
54
+ }
55
+ //# sourceMappingURL=testing-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-app.js","sourceRoot":"","sources":["../src/testing-app.ts"],"names":[],"mappings":";;AAyDA,4CAiCC;AA1FD,4BAA0B;AAC1B,+BAA4C;AAC5C,0CAAqE;AAmCrE,MAAM,QAAQ,GAAG,CAAC,GAAiB,EAAkD,EAAE,CACnF,OAAO,GAAG,KAAK,UAAU,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,gBAAgB,CAAC,OAA0B;IAC7D,MAAM,EAAE,UAAU,EAAE,SAAS,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAE5D,kFAAkF;IAClF,KAAK,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,SAAS,EAAE,CAAC;QAC5C,6BAAqB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,2CAA2C;IAC3C,MAAM,QAAQ,GAAW,EAAE,CAAC;IAC5B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAChB,MAAM,GAAG,GAAG,MAAM,GAAG,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,GAAY,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,oBAAY,CAAC,QAAe,EAAE,WAAW,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,IAAA,mBAAY,EAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAExD,OAAO;QACH,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,CAAC,YAAY,EAAE,EAAE,CAAC,6BAAqB,CAAC,GAAG,CAAC,YAAY,CAAC;QAC9D,KAAK,EAAE,GAAG,EAAE,CACR,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC1B,6BAAqB,CAAC,MAAM,EAAE,CAAC;YAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QAClC,CAAC,CAAC;KACT,CAAC;AACN,CAAC"}
@@ -0,0 +1,25 @@
1
+ export type Ctor<T = any> = new (...args: any[]) => T;
2
+ /**
3
+ * Typed facade over Dolph's process-wide GlobalServiceRegistry.
4
+ *
5
+ * `@Component` resolves and caches every service the moment the module that
6
+ * declares it is first imported — not per-request, per-test-run, or lazily.
7
+ * A mock only wins if it is already sitting in the registry before that
8
+ * import happens; otherwise the real service has already been built and
9
+ * cached first. `createTestingApp` (see `testing-app.ts`) sequences this
10
+ * correctly for you — reach for `TestingRegistry` directly only when you
11
+ * need finer control (e.g. resetting between tests in the same file).
12
+ */
13
+ export declare const TestingRegistry: {
14
+ /** Seeds a mock instance so the next `@Component` resolution picks it up instead of constructing the real service. */
15
+ override<T>(serviceClass: Ctor<T>, mockInstance: T): void;
16
+ /** Reads whatever is currently cached for this service class — the real instance or a mock. */
17
+ get<T>(serviceClass: Ctor<T>): T | undefined;
18
+ has(serviceClass: Ctor): boolean;
19
+ /**
20
+ * Clears every cached service, process-wide. Call between test files (or
21
+ * `describe` blocks that each build their own testing app) — never mid-file
22
+ * while another in-flight test still expects its services to be resolved.
23
+ */
24
+ reset(): void;
25
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestingRegistry = void 0;
4
+ const dolph_1 = require("@dolphjs/dolph");
5
+ /**
6
+ * Typed facade over Dolph's process-wide GlobalServiceRegistry.
7
+ *
8
+ * `@Component` resolves and caches every service the moment the module that
9
+ * declares it is first imported — not per-request, per-test-run, or lazily.
10
+ * A mock only wins if it is already sitting in the registry before that
11
+ * import happens; otherwise the real service has already been built and
12
+ * cached first. `createTestingApp` (see `testing-app.ts`) sequences this
13
+ * correctly for you — reach for `TestingRegistry` directly only when you
14
+ * need finer control (e.g. resetting between tests in the same file).
15
+ */
16
+ exports.TestingRegistry = {
17
+ /** Seeds a mock instance so the next `@Component` resolution picks it up instead of constructing the real service. */
18
+ override(serviceClass, mockInstance) {
19
+ dolph_1.GlobalServiceRegistry.set(serviceClass, mockInstance);
20
+ },
21
+ /** Reads whatever is currently cached for this service class — the real instance or a mock. */
22
+ get(serviceClass) {
23
+ return dolph_1.GlobalServiceRegistry.get(serviceClass);
24
+ },
25
+ has(serviceClass) {
26
+ return dolph_1.GlobalServiceRegistry.has(serviceClass);
27
+ },
28
+ /**
29
+ * Clears every cached service, process-wide. Call between test files (or
30
+ * `describe` blocks that each build their own testing app) — never mid-file
31
+ * while another in-flight test still expects its services to be resolved.
32
+ */
33
+ reset() {
34
+ dolph_1.GlobalServiceRegistry._reset();
35
+ },
36
+ };
37
+ //# sourceMappingURL=testing-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-registry.js","sourceRoot":"","sources":["../src/testing-registry.ts"],"names":[],"mappings":";;;AAAA,0CAAuD;AAIvD;;;;;;;;;;GAUG;AACU,QAAA,eAAe,GAAG;IAC3B,sHAAsH;IACtH,QAAQ,CAAI,YAAqB,EAAE,YAAe;QAC9C,6BAAqB,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAC1D,CAAC;IAED,+FAA+F;IAC/F,GAAG,CAAI,YAAqB;QACxB,OAAO,6BAAqB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC;IAED,GAAG,CAAC,YAAkB;QAClB,OAAO,6BAAqB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACH,KAAK;QACD,6BAAqB,CAAC,MAAM,EAAE,CAAC;IACnC,CAAC;CACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@dolphjs/testing",
3
+ "version": "1.0.0",
4
+ "description": "Official testing utilities for DolphJS applications — fast, isolated unit tests and lightweight HTTP integration tests built for the framework's decoration-time DI model.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "repository": "https://github.com/dolphjs/testing",
11
+ "bugs": "https://github.com/dolphjs/testing/issues",
12
+ "homepage": "https://github.com/dolphjs/testing#readme",
13
+ "author": "Utee Akaninyene",
14
+ "license": "MIT",
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc -b -v .",
23
+ "clean": "rimraf dist",
24
+ "typecheck": "tsc --noEmit",
25
+ "lint": "eslint \"src/**/*.ts\"",
26
+ "test:examples": "jest --config jest.examples.config.js"
27
+ },
28
+ "peerDependencies": {
29
+ "@dolphjs/dolph": ">=2.0.0",
30
+ "reflect-metadata": "^0.1.13 || ^0.2.0",
31
+ "mongodb-memory-server": ">=9.0.0",
32
+ "sequelize": ">=6.0.0",
33
+ "typeorm": ">=0.3.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "mongodb-memory-server": {
37
+ "optional": true
38
+ },
39
+ "sequelize": {
40
+ "optional": true
41
+ },
42
+ "typeorm": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@dolphjs/dolph": "file:../dolph/dist",
48
+ "@types/express": "^4.17.21",
49
+ "@types/jest": "^29.5.12",
50
+ "@types/node": "^20.14.0",
51
+ "@types/supertest": "^6.0.2",
52
+ "jest": "^29.7.0",
53
+ "mongodb-memory-server": "^9.2.0",
54
+ "reflect-metadata": "^0.2.2",
55
+ "rimraf": "^5.0.7",
56
+ "sequelize": "^6.37.3",
57
+ "supertest": "^7.0.0",
58
+ "ts-jest": "^29.2.3",
59
+ "typeorm": "^0.3.20",
60
+ "typescript": "^5.4.5"
61
+ }
62
+ }