@codefast/di-testing 0.1.4 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # @codefast/di-testing
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#999](https://github.com/codefastlabs/codefast/pull/999) Requires `@codefast/di` 0.12, whose chains declare a binding's slot before its strategy; the test beds bind their mocks
8
+ and real collaborators in that order.
9
+
10
+ ### Patch Changes
11
+
12
+ - [#1005](https://github.com/codefastlabs/codefast/pull/1005) **Breaking for deep subpath imports only** — the root entry `@codefast/di` is unchanged. `src/` now follows three rules,
13
+ and the published subpaths follow `src/` with no exception: a directory names a family of two or more modules and a lone
14
+ module sits flat at its layer; a directory carries the topic and a file its role, so no file repeats its directory's
15
+ name; and the exports map is the source tree, so the `strip` that kept the introspection modules at flat specifiers is
16
+ gone.
17
+
18
+ Moves: the `MetadataReader` verification leaves `resolution/cache/class-introspector` for `metadata/verify` — it was the
19
+ one value import pointing up from `metadata/` into `resolution/`; the `RESOLUTION_DIAGNOSTICS` channel leaves `errors/`
20
+ for `introspection/diagnostics`; the fluent chain's contract leaves `core/binding` for `core/binding-builders`, and the
21
+ class implementing it is `container/binding-chain`; `effectiveBindingScope` folds into `core/binding`.
22
+
23
+ Renamed specifiers: `./errors/errors` → `./errors`; `./errors/diagnostics` → `./introspection/diagnostics`;
24
+ `./ambient/active-container` → `./ambient-container`; `./container/binding-builders` → `./container/binding-chain`;
25
+ `./core/binding-scope` → gone (`effectiveBindingScope` is in `./core/binding`); `./injection/resolve-options` →
26
+ `./injection/dependency-slot`;
27
+ `./metadata/{metadata-keys,metadata-types,metadata-reader-token,symbol-metadata-reader,verifying-metadata-reader}` →
28
+ `./metadata/{keys,types,reader-token,symbol-reader,verifying-reader}`; `./lifecycle/{lifecycle-manager,scope-manager}` →
29
+ `./lifecycle/{hooks,scopes}`; `./decorators/{lifecycle-decorators,decorator-metadata}` →
30
+ `./decorators/{lifecycle,metadata-record}`; `./resolution/path/resolution-path` → `./resolution/path`;
31
+ `./resolution/plan/{instantiation-plan,plan-codegen}` → `./resolution/plan/{compiler,codegen}`;
32
+ `./resolution/select/binding-select` → `./resolution/select/candidates`; `./resolution/cache/binding-lookup-cache` →
33
+ `./resolution/cache/lookup`; `./{inspector,explanation,dependency-graph,graph-adapters/*}` →
34
+ `./introspection/{inspector,explanation,dependency-graph,graph-adapters/*}`. New: `./core/binding-builders`,
35
+ `./metadata/verify`.
36
+
37
+ `@codefast/di-testing` follows the one specifier it imports, `metadata/verifying-reader`.
38
+
39
+ - Updated dependencies:
40
+ - @codefast/di@0.12.0
41
+
42
+ ## 0.2.0
43
+
44
+ ### Minor Changes
45
+
46
+ - [#952](https://github.com/codefastlabs/codefast/pull/952) A suite now states its mock backend once, and a test bed can no longer be typed against one backend and built from
47
+ another. `createTestBed({ mockFactory, metadataReader? })` returns the entry point every bed begins from, its `Backend`
48
+ inferred from the factory; `TestBed.solitary(target)` and `TestBed.sociable(target)` take the target only. The `TestBed`
49
+ export is `createTestBed({ mockFactory: defaultMockFactory })`, the built-in spy with no framework needed.
50
+
51
+ Before, the backend was chosen per call — `TestBed.solitary(Unit, { mockFactory: () => vi.fn() })` — and
52
+ `TestBed.solitary<Unit, SinonStub>(Unit)` with no factory type-checked, typed every mock as `SinonStub`, and built the
53
+ default spy. Migrate by creating the suite's entry point once:
54
+
55
+ ```ts
56
+ import { createTestBed } from "@codefast/di-testing";
57
+ import { vi } from "vitest";
58
+
59
+ export const TestBed = createTestBed({ mockFactory: () => vi.fn() });
60
+ ```
61
+
62
+ `TestBedOptions<Backend>` configures `createTestBed` and requires `mockFactory`; `TestBedStatic<Backend>` names the
63
+ entry point's backend and has no default. `createTestBed` throws the new `MissingMockFactoryError`
64
+ (`MISSING_MOCK_FACTORY`) for a caller past the types who passes no factory, instead of standing the built-in spy in for
65
+ a backend nobody chose.
66
+
67
+ - [#976](https://github.com/codefastlabs/codefast/pull/976) `engines.node` is now `>=24.0.0`, up from `>=22.12.0`, and Node 22 is no longer supported. Node 24.0.0 is the first
68
+ release with explicit resource management built in (`using`, `await using`, `DisposableStack`, `AsyncDisposableStack`,
69
+ `SuppressedError`) and all of ES2025, so the packages use both as the platform ships them instead of shimming them for
70
+ an older line, and the CI matrix runs the unit suite on 24.0.0 itself. Move to Node 24, or stay on the current minor
71
+ while a deployment still runs Node 22.
72
+
73
+ ### Patch Changes
74
+
75
+ - [#976](https://github.com/codefastlabs/codefast/pull/976) The README states what a program needs for the declarations' disposal members: the explicit resource management types,
76
+ which no numbered `lib` declares before ES2027. `@types/node` 24 or later loads them, and so does `ESNext.Disposable` in
77
+ `lib`. Without either, TypeScript 7 fails inside `container.d.ts` with TS2550 under `skipLibCheck: false`, and at
78
+ `await using` with TS2318. `@codefast/di-testing` drops a `/// <reference lib="esnext.disposable" />` that TypeScript 7
79
+ stripped from its emitted declarations anyway, and takes the lib from its `tsconfig.json` instead.
80
+
81
+ - [#956](https://github.com/codefastlabs/codefast/pull/956) `README.md` no longer quotes the `@codefast/di` peer range, which had fallen behind `package.json` — the release tooling
82
+ moves that range, so the manifest is the one place it is stated.
83
+
84
+ - [#965](https://github.com/codefastlabs/codefast/pull/965) `README.md` now states TypeScript 7 or later as the floor for the package's types — the one compiler every `@codefast/*`
85
+ package is built and checked with. No code or declaration changed.
86
+ - Updated dependencies:
87
+ - @codefast/di@0.11.0
88
+
3
89
  ## 0.1.4
4
90
 
5
91
  ### Patch Changes
package/README.md CHANGED
@@ -19,11 +19,13 @@ production — and you assert against the mocks the bed created.
19
19
  and builds a mock for each — no per-collaborator `bind(...).toConstantValue(...)`.
20
20
  - **Real instance, real wiring.** The unit is constructed through a container, so `@postConstruct`, accessor injection,
21
21
  and `@preDestroy` run exactly as in production.
22
- - **Zero test-framework dependency.** The default mock is a small built-in spy. Pass `() => vi.fn()` — or `jest.fn`,
23
- `() => sinon.stub()` — to build the mocks from that backend and use its matchers instead.
24
- - **Backend-typed lookups.** The factory's return type flows through the whole bed: with `() => vi.fn()`,
25
- `mocks.get(EmailToken).send` carries Vitest's own mock surface (`mockReturnValueOnce`, `mockClear`, and so on), with
26
- no adapter package and no module augmentation.
22
+ - **Zero test-framework dependency.** The default `TestBed` mocks with a small built-in spy. Create your suite's own
23
+ with `createTestBed({ mockFactory: () => vi.fn() })` — or `jest.fn`, `() => sinon.stub()` — to build every mock from
24
+ that backend and use its matchers instead.
25
+ - **Backend-typed lookups.** The factory's return type flows through every bed the entry point begins: with
26
+ `() => vi.fn()`, `mocks.get(EmailToken).send` carries Vitest's own mock surface (`mockReturnValueOnce`, `mockClear`,
27
+ and so on), with no adapter package and no module augmentation. A bed can have no other backend than its entry point,
28
+ so a mock is never typed as one backend and built from another.
27
29
 
28
30
  ## Installation
29
31
 
@@ -31,17 +33,21 @@ production — and you assert against the mocks the bed created.
31
33
  pnpm add -D @codefast/di-testing
32
34
  ```
33
35
 
34
- `@codefast/di-testing` requires Node.js 22.12 or later and a peer install of `@codefast/di` (`>=0.8.0`), with the same
35
- TypeScript setup: native Stage 3 decorators, `experimentalDecorators` off. The package is published on 0.x and versioned
36
- on its own track: breaking changes ship as minor versions, so pin the minor version when you need stability.
36
+ `@codefast/di-testing` requires Node.js 24 or later, TypeScript 7 or later, and a peer install of `@codefast/di`, with
37
+ the same TypeScript setup: native Stage 3 decorators, `experimentalDecorators` off, and the explicit resource management
38
+ types, from `@types/node` 24 or later or from `ESNext.Disposable` in `lib`. The package is published on 0.x and
39
+ versioned on its own track: breaking changes ship as minor versions, so pin the minor version when you need stability.
37
40
 
38
41
  ## Quick start
39
42
 
40
43
  ```ts
41
44
  import { injectable, token } from "@codefast/di";
42
- import { TestBed } from "@codefast/di-testing";
45
+ import { createTestBed } from "@codefast/di-testing";
43
46
  import { expect, it, vi } from "vitest";
44
47
 
48
+ // The suite's entry point on Vitest spies, so matchers and mockReturnValue work on every mock.
49
+ const TestBed = createTestBed({ mockFactory: () => vi.fn() });
50
+
45
51
  interface UserService {
46
52
  findUser(id: string): { id: string; email: string };
47
53
  }
@@ -73,7 +79,7 @@ class OrderProcessor {
73
79
  }
74
80
 
75
81
  it("charges then emails a confirmation", () => {
76
- const { unit, mocks } = TestBed.solitary(OrderProcessor, { mockFactory: () => vi.fn() })
82
+ const { unit, mocks } = TestBed.solitary(OrderProcessor)
77
83
  .mock(UserServiceToken)
78
84
  .stub((fn) => ({ findUser: fn().mockReturnValue({ id: "u1", email: "alice@example.com" }) }))
79
85
  .compile();
@@ -85,8 +91,8 @@ it("charges then emails a confirmation", () => {
85
91
  });
86
92
  ```
87
93
 
88
- The zero-dependency default reads the same, minus the `mockFactory`. Assert against the built-in spy's `.mock.calls` and
89
- stub with `.mockReturnValue()`:
94
+ The zero-dependency default is the `TestBed` export itself. Assert against the built-in spy's `.mock.calls` and stub
95
+ with `.mockReturnValue()`:
90
96
 
91
97
  ```ts
92
98
  import { TestBed } from "@codefast/di-testing";
@@ -97,14 +103,29 @@ unit.placeOrder("u1", 42);
97
103
  assert.deepEqual(mocks.get(PaymentGatewayToken).charge.mock.calls[0], ["u1", 42]);
98
104
  ```
99
105
 
100
- ## Solitary beds
106
+ ## Entry points
107
+
108
+ A suite states its mock backend once. `createTestBed(options)` returns the entry point every bed in the suite begins
109
+ from — typically created in one shared test-support module:
101
110
 
102
- `TestBed.solitary(target, options?)` begins a bed for `target` and auto-mocks every dependency it declares. Nothing is
103
- instantiated until you call `compile()`. The options:
111
+ ```ts
112
+ import { createTestBed } from "@codefast/di-testing";
113
+ import { vi } from "vitest";
114
+
115
+ export const TestBed = createTestBed({ mockFactory: () => vi.fn() });
116
+ ```
104
117
 
105
- - `mockFactory?: () => spy` — the spy backend each auto-mock is built from. Defaults to the built-in spy.
118
+ - `mockFactory: () => spy` — required: the spy backend each auto-mock is built from. Its return type is the entry
119
+ point's backend and types every bed it begins. Pass `defaultMockFactory` for the built-in spy.
106
120
  - `metadataReader?: MetadataReader` — the reader dependencies are discovered through. Defaults to di's reader.
107
121
 
122
+ The `TestBed` export is `createTestBed({ mockFactory: defaultMockFactory })`: the built-in spy, no framework needed.
123
+
124
+ ## Solitary beds
125
+
126
+ `TestBed.solitary(target)` begins a bed for `target` and auto-mocks every dependency it declares with the entry point's
127
+ backend. Nothing is instantiated until you call `compile()`.
128
+
108
129
  The builder records overrides, then compiles:
109
130
 
110
131
  - `.mock(token).stub((fn) => stub)` — bind a partial stub built from the active spy factory; unlisted members stay
@@ -127,14 +148,17 @@ The builder records overrides, then compiles:
127
148
  ## Sociable beds
128
149
 
129
150
  A sociable bed keeps chosen collaborators real while everything else stays mocked — a unit test over a small real
130
- subtree, not an integration test. `TestBed.sociable(target, options?)` takes the same options and returns only
131
- `.expose()`, because a sociable bed with nothing exposed is a solitary bed.
151
+ subtree, not an integration test. `TestBed.sociable(target)` mocks with the entry point's backend like `solitary` and
152
+ returns only `.expose()`, because a sociable bed with nothing exposed is a solitary bed.
132
153
 
133
154
  ```ts
134
155
  import { injectable, token } from "@codefast/di";
135
- import { TestBed } from "@codefast/di-testing";
156
+ import { createTestBed } from "@codefast/di-testing";
136
157
  import { expect, it, vi } from "vitest";
137
158
 
159
+ // The suite's entry point on Vitest spies, so matchers and mockReturnValue work on every mock.
160
+ const TestBed = createTestBed({ mockFactory: () => vi.fn() });
161
+
138
162
  interface TaxPolicy {
139
163
  rateFor(currency: string): number;
140
164
  }
@@ -160,7 +184,7 @@ class CheckoutService {
160
184
  }
161
185
 
162
186
  it("prices through the real PricingService over a mocked tax boundary", () => {
163
- const bed = TestBed.sociable(CheckoutService, { mockFactory: () => vi.fn() })
187
+ const bed = TestBed.sociable(CheckoutService)
164
188
  .expose(PricingService)
165
189
  .mock(TaxPolicyToken)
166
190
  .stub((fn) => ({ rateFor: fn().mockReturnValue(0.1) }))
@@ -206,10 +230,10 @@ it("prices through the real PricingService over a mocked tax boundary", () => {
206
230
  - `dispose()` — run the unit's `@preDestroy` hooks and dispose the container.
207
231
 
208
232
  The bed implements `AsyncDisposable`, so `await using bed = TestBed.solitary(X).compile()` disposes it at the end of the
209
- block; that needs the `esnext.disposable` lib in your TypeScript configuration if your `target` does not include it.
233
+ block.
210
234
 
211
235
  The lower-level pieces are exported too: `createAutoMock`, `createSpy`, `defaultMockFactory`, and the `Mocked`,
212
- `DeepPartial`, `MockFactory`, and `Spy` types.
236
+ `DeepPartial`, `MockFactory`, `Spy`, and `TestBedOptions` types.
213
237
 
214
238
  ## Errors
215
239
 
@@ -227,7 +251,7 @@ extending it, so setup failures can be caught separately from resolution failure
227
251
  ## Documentation
228
252
 
229
253
  - [Rendered docs on codefastlabs.com](https://codefastlabs.com/docs/di-testing)
230
- - [`@codefast/di`](../di/README.md) — the container this package builds on, including its `SPEC.md`.
254
+ - [`@codefast/di`](../di/README.md) — the container this package builds on, including its specification under `spec/`.
231
255
  - [`CHANGELOG.md`](./CHANGELOG.md) — release history.
232
256
 
233
257
  ## Contributing
@@ -127,7 +127,7 @@ export function bindMocks(container, dependencies, overrides, mockFactory, expos
127
127
  }
128
128
  };
129
129
  const bindConstant = (slot, value) => {
130
- const builder = container.bind(slot.token).toConstantValue(value);
130
+ const builder = container.bind(slot.token);
131
131
  if (slot.name !== undefined) {
132
132
  builder.whenNamed(slot.name);
133
133
  }
@@ -136,6 +136,7 @@ export function bindMocks(container, dependencies, overrides, mockFactory, expos
136
136
  builder.whenTagged(tag);
137
137
  }
138
138
  }
139
+ builder.toConstantValue(value);
139
140
  };
140
141
  for (const slot of dependencies) {
141
142
  const list = overrides.get(slot.token) ?? [];
@@ -167,8 +168,8 @@ export function bindMocks(container, dependencies, overrides, mockFactory, expos
167
168
  for (const [index, value] of values.entries()) {
168
169
  container
169
170
  .bind(slot.token)
170
- .toConstantValue(value)
171
- .whenNamed(`${ALL_ELEMENT_SLOT_PREFIX}${String(index)}`);
171
+ .whenNamed(`${ALL_ELEMENT_SLOT_PREFIX}${String(index)}`)
172
+ .toConstantValue(value);
172
173
  }
173
174
  record(slot.token, { criteria: override.criteria, value: values, kind: "all" });
174
175
  }
@@ -21,6 +21,18 @@ export declare class NotInjectableError extends TestingError {
21
21
  readonly targetName: string;
22
22
  constructor(targetName: string);
23
23
  }
24
+ /**
25
+ * A `createTestBed(...)` call with no mock factory to build its beds' mocks from.
26
+ *
27
+ * @remarks The types require the factory; this reaches a caller the compiler cannot, and stops the
28
+ * built-in spy from standing in for a backend nobody chose.
29
+ *
30
+ * @since 0.2.0
31
+ */
32
+ export declare class MissingMockFactoryError extends TestingError {
33
+ readonly code = "MISSING_MOCK_FACTORY";
34
+ constructor();
35
+ }
24
36
  /**
25
37
  * A `.mock(...)` override or a `mocks.get(...)` lookup named a token or slot the unit does not use.
26
38
  *
@@ -26,6 +26,20 @@ export class NotInjectableError extends TestingError {
26
26
  this.targetName = targetName;
27
27
  }
28
28
  }
29
+ /**
30
+ * A `createTestBed(...)` call with no mock factory to build its beds' mocks from.
31
+ *
32
+ * @remarks The types require the factory; this reaches a caller the compiler cannot, and stops the
33
+ * built-in spy from standing in for a backend nobody chose.
34
+ *
35
+ * @since 0.2.0
36
+ */
37
+ export class MissingMockFactoryError extends TestingError {
38
+ code = "MISSING_MOCK_FACTORY";
39
+ constructor() {
40
+ super("createTestBed() needs a mockFactory to build every bed's mocks from: pass defaultMockFactory for the built-in spy, or a factory such as () => vi.fn().");
41
+ }
42
+ }
29
43
  /**
30
44
  * A `.mock(...)` override or a `mocks.get(...)` lookup named a token or slot the unit does not use.
31
45
  *
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Solitary and sociable auto-mocking test beds for `@codefast/di`. */
2
- export { TestBed } from "#test-bed/test-bed";
2
+ export { createTestBed, TestBed } from "#test-bed/test-bed";
3
3
  export type { TestBedStatic } from "#test-bed/test-bed";
4
4
  export type { MockOverrideBuilder, PreparedBed, TestBedOptions } from "#test-bed/bed-builder";
5
5
  export type { SolitaryTestBedBuilder } from "#test-bed/solitary-builder";
@@ -12,5 +12,5 @@ export type { MockFactory, MockFunction } from "#mocking/mock-factory";
12
12
  export { createSpy } from "#mocking/spy";
13
13
  export type { Spy, SpyResult, SpyState } from "#mocking/spy";
14
14
  export type { InjectionIdentifier } from "#types";
15
- export { ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#errors/errors";
15
+ export { MissingMockFactoryError, ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#errors/errors";
16
16
  export type { SealedCause } from "#errors/errors";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Solitary and sociable auto-mocking test beds for `@codefast/di`. */
2
- export { TestBed } from "#test-bed/test-bed";
2
+ export { createTestBed, TestBed } from "#test-bed/test-bed";
3
3
  export { createAutoMock, MOCK_RESET } from "#mocking/auto-mock";
4
4
  export { defaultMockFactory } from "#mocking/mock-factory";
5
5
  export { createSpy } from "#mocking/spy";
6
- export { ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#errors/errors";
6
+ export { MissingMockFactoryError, ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#errors/errors";
@@ -6,16 +6,16 @@ import type { MockFactory, MockFunction } from "#mocking/mock-factory";
6
6
  import type { Spy } from "#mocking/spy";
7
7
  import type { InjectionIdentifier } from "#types";
8
8
  /**
9
- * Options that configure a whole test-bed compile.
9
+ * What `createTestBed` is configured with: the mock backend and the reader every bed it begins shares.
10
10
  *
11
11
  * @typeParam Backend - The spy type the mock factory produces; it flows into every `Mocked` member,
12
12
  * `mocks.get`, and the `.stub` callback, so `() => vi.fn()` yields Vitest's own mock typing.
13
13
  *
14
14
  * @since 0.1.0
15
15
  */
16
- export interface TestBedOptions<Backend extends MockFunction = Spy> {
17
- /** Spy factory each auto-mock property is materialized with; defaults to the built-in spy. */
18
- readonly mockFactory?: MockFactory<Backend> | undefined;
16
+ export interface TestBedOptions<Backend extends MockFunction> {
17
+ /** Spy factory each auto-mock property is materialized with; its return type is the beds' backend. */
18
+ readonly mockFactory: MockFactory<Backend>;
19
19
  /** Reader the dependency scan and the compile container both consult; defaults to di's reader. */
20
20
  readonly metadataReader?: MetadataReader | undefined;
21
21
  }
@@ -74,7 +74,7 @@ export declare abstract class BedBuilder<Class, Backend extends MockFunction = S
74
74
  protected readonly reader: MetadataReader;
75
75
  protected readonly mockFactory: MockFactory<Backend>;
76
76
  protected readonly overrides: ReadonlyMap<DependencyKey, ReadonlyArray<SlottedOverride>>;
77
- constructor(target: Constructor<Class>, options?: TestBedOptions<Backend>);
77
+ constructor(target: Constructor<Class>, options: TestBedOptions<Backend>);
78
78
  /**
79
79
  * Replaces the auto-mock for one dependency with a hand-written stub or a concrete value.
80
80
  *
@@ -1,8 +1,7 @@
1
1
  /** The override-recording core both test-bed builders extend. */
2
2
  import { defaultMetadataReader } from "@codefast/di";
3
- import { verifyingMetadataReader } from "@codefast/di/metadata/verifying-metadata-reader";
3
+ import { verifyingMetadataReader } from "@codefast/di/metadata/verifying-reader";
4
4
  import { criteriaEquals, normalizeCriteria } from "#discovery/mock-binder";
5
- import { defaultMockFactory } from "#mocking/mock-factory";
6
5
  /**
7
6
  * The shared builder core: records overrides, resolves the reader and mock factory, and owns the
8
7
  * compile template that disposes the container when a build fails.
@@ -23,9 +22,8 @@ export class BedBuilder {
23
22
  constructor(target, options) {
24
23
  this.target = target;
25
24
  // A supplied reader is a claim — verify it the way the container itself does.
26
- this.reader = verifyingMetadataReader(options?.metadataReader ?? defaultMetadataReader);
27
- // With no factory the caller's Backend defaulted to Spy, which is what the default produces.
28
- this.mockFactory = options?.mockFactory ?? defaultMockFactory;
25
+ this.reader = verifyingMetadataReader(options.metadataReader ?? defaultMetadataReader);
26
+ this.mockFactory = options.mockFactory;
29
27
  }
30
28
  /**
31
29
  * Replaces the auto-mock for one dependency with a hand-written stub or a concrete value.
@@ -61,13 +61,14 @@ export class SociableBuilder extends BedBuilder {
61
61
  container.bind(realToken).to(realClass).singleton();
62
62
  container.bind(realClass).toDynamic((context) => context.resolve(realToken));
63
63
  for (const slot of slots) {
64
- const builder = container.bind(realClass).toDynamic((context) => context.resolve(realToken));
64
+ const builder = container.bind(realClass);
65
65
  if (slot.name !== undefined) {
66
66
  builder.whenNamed(slot.name);
67
67
  }
68
68
  for (const tag of slot.tags ?? []) {
69
69
  builder.whenTagged(tag);
70
70
  }
71
+ builder.toDynamic((context) => context.resolve(realToken));
71
72
  }
72
73
  }
73
74
  // Sealed entries so mocks.get points at bed.exposed() instead of handing back a fake Mocked.
@@ -1,4 +1,4 @@
1
- /** The entry point for building isolated units under test. */
1
+ /** The entry points for building isolated units under test. */
2
2
  import type { Constructor } from "@codefast/di";
3
3
  import type { MockFunction } from "#mocking/mock-factory";
4
4
  import type { Spy } from "#mocking/spy";
@@ -6,33 +6,43 @@ import type { TestBedOptions } from "#test-bed/bed-builder";
6
6
  import type { SociableTestBedBuilder } from "#test-bed/sociable-builder";
7
7
  import type { SolitaryTestBedBuilder } from "#test-bed/solitary-builder";
8
8
  /**
9
- * The factory surface for building isolated units under test.
9
+ * Begins test beds for classes under test, every one of them mocked with the same backend.
10
+ *
11
+ * @typeParam Backend - The spy type every bed's mocks are built with, fixed when the entry point is created.
10
12
  *
11
13
  * @since 0.1.0
12
14
  */
13
- export interface TestBedStatic {
15
+ export interface TestBedStatic<Backend extends MockFunction> {
14
16
  /**
15
17
  * Begins a solitary test bed for `target`, auto-mocking every dependency it declares.
16
- *
17
- * @remarks The mock factory's return type becomes `Backend` and types every mock the bed hands
18
- * out — pass `{ mockFactory: () => vi.fn() }` and `mocks.get(X).method` carries Vitest's own
19
- * mock surface.
20
18
  */
21
- solitary<Class, Backend extends MockFunction = Spy>(target: Constructor<Class>, options?: TestBedOptions<Backend>): SolitaryTestBedBuilder<Class, Backend>;
19
+ solitary<Class>(target: Constructor<Class>): SolitaryTestBedBuilder<Class, Backend>;
22
20
  /**
23
21
  * Begins a sociable test bed for `target`: chosen class collaborators stay real, tokens stay mocked.
24
22
  *
25
23
  * @remarks Returns only `expose` — a sociable bed without at least one exposed collaborator is a
26
24
  * solitary bed, so the type steers the first call.
27
25
  */
28
- sociable<Class, Backend extends MockFunction = Spy>(target: Constructor<Class>, options?: TestBedOptions<Backend>): Pick<SociableTestBedBuilder<Class, Backend>, "expose">;
26
+ sociable<Class>(target: Constructor<Class>): Pick<SociableTestBedBuilder<Class, Backend>, "expose">;
29
27
  }
30
28
  /**
31
- * Entry point for auto-mocking a class in isolation from its collaborators.
29
+ * Creates the test-bed entry point for one mock backend, so a suite states its backend once.
30
+ *
31
+ * @remarks The factory's return type becomes `Backend` and types every mock the beds hand out —
32
+ * `createTestBed({ mockFactory: () => vi.fn() })` gives `mocks.get(X).method` Vitest's own mock
33
+ * surface. A bed can have no other backend than the entry point it was begun from.
34
+ *
35
+ * @throws MissingMockFactoryError When no factory is passed, which only a caller past the types can do.
36
+ *
37
+ * @since 0.2.0
38
+ */
39
+ export declare function createTestBed<Backend extends MockFunction>(options: TestBedOptions<Backend>): TestBedStatic<Backend>;
40
+ /**
41
+ * The test-bed entry point on the built-in spy, the backend with no test-framework dependency.
32
42
  *
33
- * @remarks `solitary` and `sociable` record the target and options only; nothing is instantiated
34
- * until `compile()`.
43
+ * @remarks `solitary` and `sociable` record the target only; nothing is instantiated until
44
+ * `compile()`. A suite on another backend creates its own with `createTestBed`.
35
45
  *
36
46
  * @since 0.1.0
37
47
  */
38
- export declare const TestBed: TestBedStatic;
48
+ export declare const TestBed: TestBedStatic<Spy>;
@@ -1,19 +1,37 @@
1
- /** The entry point for building isolated units under test. */
1
+ /** The entry points for building isolated units under test. */
2
+ import { MissingMockFactoryError } from "#errors/errors";
3
+ import { defaultMockFactory } from "#mocking/mock-factory";
2
4
  import { SociableBuilder } from "#test-bed/sociable-builder";
3
5
  import { SolitaryBuilder } from "#test-bed/solitary-builder";
4
6
  /**
5
- * Entry point for auto-mocking a class in isolation from its collaborators.
7
+ * Creates the test-bed entry point for one mock backend, so a suite states its backend once.
6
8
  *
7
- * @remarks `solitary` and `sociable` record the target and options only; nothing is instantiated
8
- * until `compile()`.
9
+ * @remarks The factory's return type becomes `Backend` and types every mock the beds hand out —
10
+ * `createTestBed({ mockFactory: () => vi.fn() })` gives `mocks.get(X).method` Vitest's own mock
11
+ * surface. A bed can have no other backend than the entry point it was begun from.
12
+ *
13
+ * @throws MissingMockFactoryError When no factory is passed, which only a caller past the types can do.
14
+ *
15
+ * @since 0.2.0
16
+ */
17
+ export function createTestBed(options) {
18
+ // Read once, so a later write to the caller's object cannot change the beds already begun.
19
+ const { mockFactory, metadataReader } = options;
20
+ if (typeof mockFactory !== "function") {
21
+ throw new MissingMockFactoryError();
22
+ }
23
+ const resolved = { mockFactory, metadataReader };
24
+ return {
25
+ solitary: (target) => new SolitaryBuilder(target, resolved),
26
+ sociable: (target) => new SociableBuilder(target, resolved),
27
+ };
28
+ }
29
+ /**
30
+ * The test-bed entry point on the built-in spy, the backend with no test-framework dependency.
31
+ *
32
+ * @remarks `solitary` and `sociable` record the target only; nothing is instantiated until
33
+ * `compile()`. A suite on another backend creates its own with `createTestBed`.
9
34
  *
10
35
  * @since 0.1.0
11
36
  */
12
- export const TestBed = {
13
- solitary(target, options) {
14
- return new SolitaryBuilder(target, options);
15
- },
16
- sociable(target, options) {
17
- return new SociableBuilder(target, options);
18
- },
19
- };
37
+ export const TestBed = createTestBed({ mockFactory: defaultMockFactory });
@@ -1,4 +1,4 @@
1
- /// <reference lib="esnext.disposable" />
1
+ /** The compiled result of a test bed: the real unit plus handles to its mocks. */
2
2
  import { tokenName } from "@codefast/di";
3
3
  import { criteriaEquals, normalizeCriteria } from "#discovery/mock-binder";
4
4
  import { ExposureError, SealedDependencyError, UndeclaredDependencyError } from "#errors/errors";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/di-testing",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "Solitary and sociable auto-mocking test beds for @codefast/di",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -100,9 +100,9 @@
100
100
  "access": "public"
101
101
  },
102
102
  "peerDependencies": {
103
- "@codefast/di": ">=0.10.1"
103
+ "@codefast/di": ">=0.12.0"
104
104
  },
105
105
  "engines": {
106
- "node": ">=22.12.0"
106
+ "node": ">=24.0.0"
107
107
  }
108
108
  }