@codefast/di-testing 0.1.0 → 0.1.2
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 +25 -0
- package/README.md +140 -63
- package/dist/discovery/dependency-scanner.d.ts +1 -2
- package/dist/discovery/dependency-scanner.js +1 -2
- package/dist/discovery/mock-binder.d.ts +3 -3
- package/dist/discovery/mock-binder.js +26 -12
- package/dist/errors/errors.d.ts +1 -2
- package/dist/errors/errors.js +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/mocking/auto-mock.d.ts +1 -2
- package/dist/mocking/auto-mock.js +1 -2
- package/dist/mocking/mock-factory.d.ts +1 -2
- package/dist/mocking/mock-factory.js +1 -2
- package/dist/mocking/spy.d.ts +1 -2
- package/dist/mocking/spy.js +1 -2
- package/dist/test-bed/bed-builder.d.ts +1 -2
- package/dist/test-bed/bed-builder.js +1 -2
- package/dist/test-bed/sociable-builder.d.ts +1 -2
- package/dist/test-bed/sociable-builder.js +1 -2
- package/dist/test-bed/solitary-builder.d.ts +1 -2
- package/dist/test-bed/solitary-builder.js +1 -2
- package/dist/test-bed/test-bed.d.ts +1 -2
- package/dist/test-bed/test-bed.js +1 -2
- package/dist/test-bed/unit-test-bed.d.ts +1 -2
- package/dist/test-bed/unit-test-bed.js +1 -2
- package/dist/types.d.ts +1 -2
- package/dist/types.js +1 -2
- package/package.json +7 -29
- package/dist/discovery/dependency-scanner.d.ts.map +0 -1
- package/dist/discovery/dependency-scanner.js.map +0 -1
- package/dist/discovery/mock-binder.d.ts.map +0 -1
- package/dist/discovery/mock-binder.js.map +0 -1
- package/dist/errors/errors.d.ts.map +0 -1
- package/dist/errors/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mocking/auto-mock.d.ts.map +0 -1
- package/dist/mocking/auto-mock.js.map +0 -1
- package/dist/mocking/mock-factory.d.ts.map +0 -1
- package/dist/mocking/mock-factory.js.map +0 -1
- package/dist/mocking/spy.d.ts.map +0 -1
- package/dist/mocking/spy.js.map +0 -1
- package/dist/test-bed/bed-builder.d.ts.map +0 -1
- package/dist/test-bed/bed-builder.js.map +0 -1
- package/dist/test-bed/sociable-builder.d.ts.map +0 -1
- package/dist/test-bed/sociable-builder.js.map +0 -1
- package/dist/test-bed/solitary-builder.d.ts.map +0 -1
- package/dist/test-bed/solitary-builder.js.map +0 -1
- package/dist/test-bed/test-bed.d.ts.map +0 -1
- package/dist/test-bed/test-bed.js.map +0 -1
- package/dist/test-bed/unit-test-bed.d.ts.map +0 -1
- package/dist/test-bed/unit-test-bed.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/src/discovery/dependency-scanner.ts +0 -80
- package/src/discovery/mock-binder.ts +0 -315
- package/src/errors/errors.ts +0 -119
- package/src/index.ts +0 -26
- package/src/mocking/auto-mock.ts +0 -243
- package/src/mocking/mock-factory.ts +0 -35
- package/src/mocking/spy.ts +0 -92
- package/src/test-bed/bed-builder.ts +0 -172
- package/src/test-bed/sociable-builder.ts +0 -148
- package/src/test-bed/solitary-builder.ts +0 -70
- package/src/test-bed/test-bed.ts +0 -64
- package/src/test-bed/unit-test-bed.ts +0 -161
- package/src/types.ts +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @codefast/di-testing
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#791](https://github.com/codefastlabs/codefast/pull/791) [`37a212b`](https://github.com/codefastlabs/codefast/commit/37a212b4d805588413159e11e872b98db82326bf) Thanks [@thevuong](https://github.com/thevuong)! - Point the README's license badge and "License" section at the package's own `LICENSE` file instead of the monorepo root.
|
|
8
|
+
|
|
9
|
+
- [#796](https://github.com/codefastlabs/codefast/pull/796) [`ba04d27`](https://github.com/codefastlabs/codefast/commit/ba04d2703c59a1677f52e6a9fffd0ec202328218) Thanks [@thevuong](https://github.com/thevuong)! - Rewrite the README around one structure — what the package is, installation and requirements, a complete quick start,
|
|
10
|
+
the concepts in learning order, then the documents and the license — with every API claim checked against the current
|
|
11
|
+
code and no figures that go stale.
|
|
12
|
+
|
|
13
|
+
- [#784](https://github.com/codefastlabs/codefast/pull/784) [`ad2f93a`](https://github.com/codefastlabs/codefast/commit/ad2f93a688e99c3ed8be6ceeae9d6cdd6be861bc) Thanks [@thevuong](https://github.com/thevuong)! - Ship the MIT `LICENSE` file in the published package. `files` already listed it, but the file was missing from the
|
|
14
|
+
package directory, so the tarball had none.
|
|
15
|
+
- Updated dependencies [[`37a212b`](https://github.com/codefastlabs/codefast/commit/37a212b4d805588413159e11e872b98db82326bf), [`ba04d27`](https://github.com/codefastlabs/codefast/commit/ba04d2703c59a1677f52e6a9fffd0ec202328218), [`ad2f93a`](https://github.com/codefastlabs/codefast/commit/ad2f93a688e99c3ed8be6ceeae9d6cdd6be861bc)]:
|
|
16
|
+
- @codefast/di@0.8.1
|
|
17
|
+
|
|
18
|
+
## 0.1.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- [#774](https://github.com/codefastlabs/codefast/pull/774) [`176e95a`](https://github.com/codefastlabs/codefast/commit/176e95a32b44fa16d81a22e4a48f53176837c839) Thanks [@thevuong](https://github.com/thevuong)! - Fold di's reserved `slotName` criterion into slot addressing, so `{ name: "x" }` and `{ tag: slotName.of("x") }` — one
|
|
23
|
+
slot to the container — are one slot to TestBed too: a mock registered with either spelling now matches a dependency
|
|
24
|
+
declared with the other, and `mocks.get(token, options)` accepts both.
|
|
25
|
+
- Updated dependencies [[`96af502`](https://github.com/codefastlabs/codefast/commit/96af502ed8dd7fc02c4440d03b40dc6677b7bcec)]:
|
|
26
|
+
- @codefast/di@0.8.0
|
|
27
|
+
|
|
3
28
|
## 0.1.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
# @codefast/di-testing
|
|
2
2
|
|
|
3
|
-
Solitary and sociable auto-mocking test beds for
|
|
4
|
-
|
|
5
|
-
`@injectable` class in two lines, with every collaborator mocked for you, or keep chosen collaborators real.
|
|
3
|
+
Solitary and sociable auto-mocking test beds for [`@codefast/di`](../di): unit-test an `@injectable` class with every
|
|
4
|
+
collaborator mocked for you, or keep chosen collaborators real.
|
|
6
5
|
|
|
7
6
|
[](https://www.npmjs.com/package/@codefast/di-testing)
|
|
8
|
-
[](
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`@codefast/di-testing` gives you two auto-mocking test beds for [`@codefast/di`](../di). A **solitary** bed constructs
|
|
12
|
+
the class under test with every collaborator mocked for you; a **sociable** bed keeps chosen collaborators real and
|
|
13
|
+
mocks the rest.
|
|
14
|
+
|
|
15
|
+
The unit is built through a real container, so `@postConstruct`, accessor injection, and `@preDestroy` run exactly as in
|
|
16
|
+
production — and you assert against the mocks the bed created.
|
|
9
17
|
|
|
10
18
|
- **Auto-mocking.** `TestBed.solitary(Class)` reads the class's declared dependencies through di's own `MetadataReader`
|
|
11
19
|
and builds a mock for each — no per-collaborator `bind(...).toConstantValue(...)`.
|
|
12
20
|
- **Real instance, real wiring.** The unit is constructed through a container, so `@postConstruct`, accessor injection,
|
|
13
21
|
and `@preDestroy` run exactly as in production.
|
|
14
|
-
- **Zero test-framework dependency.** The default mock is a small built-in spy. Pass `() => vi.fn()`
|
|
15
|
-
`() => sinon.stub()`
|
|
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.
|
|
16
24
|
- **Backend-typed lookups.** The factory's return type flows through the whole bed: with `() => vi.fn()`,
|
|
17
|
-
`mocks.get(EmailToken).send` carries Vitest's own mock surface (`mockReturnValueOnce`, `mockClear`,
|
|
18
|
-
package
|
|
19
|
-
|
|
20
|
-
## Requirements
|
|
21
|
-
|
|
22
|
-
- **Node.js 24 or later**, matching `@codefast/di`.
|
|
23
|
-
- A peer install of `@codefast/di` with `@injectable` / `inject` classes to test.
|
|
24
|
-
- Types for `AsyncDisposable` (`await using`): `@types/node` provides them; without it, add `"ESNext.Disposable"` to
|
|
25
|
-
your tsconfig `lib`.
|
|
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.
|
|
26
27
|
|
|
27
28
|
## Installation
|
|
28
29
|
|
|
@@ -30,18 +31,32 @@ Solitary and sociable auto-mocking test beds for
|
|
|
30
31
|
pnpm add -D @codefast/di-testing
|
|
31
32
|
```
|
|
32
33
|
|
|
34
|
+
`@codefast/di-testing` requires Node.js 24 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.
|
|
37
|
+
|
|
33
38
|
## Quick start
|
|
34
39
|
|
|
35
|
-
```
|
|
36
|
-
import {
|
|
40
|
+
```ts
|
|
41
|
+
import { injectable, token } from "@codefast/di";
|
|
37
42
|
import { TestBed } from "@codefast/di-testing";
|
|
38
43
|
import { expect, it, vi } from "vitest";
|
|
39
44
|
|
|
45
|
+
interface UserService {
|
|
46
|
+
findUser(id: string): { id: string; email: string };
|
|
47
|
+
}
|
|
48
|
+
interface PaymentGateway {
|
|
49
|
+
charge(userId: string, amount: number): void;
|
|
50
|
+
}
|
|
51
|
+
interface EmailService {
|
|
52
|
+
send(to: string, body: string): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
const UserServiceToken = token<UserService>("UserService");
|
|
41
56
|
const PaymentGatewayToken = token<PaymentGateway>("PaymentGateway");
|
|
42
57
|
const EmailServiceToken = token<EmailService>("EmailService");
|
|
43
58
|
|
|
44
|
-
@injectable([
|
|
59
|
+
@injectable([UserServiceToken, PaymentGatewayToken, EmailServiceToken])
|
|
45
60
|
class OrderProcessor {
|
|
46
61
|
constructor(
|
|
47
62
|
private readonly users: UserService,
|
|
@@ -70,10 +85,10 @@ it("charges then emails a confirmation", () => {
|
|
|
70
85
|
});
|
|
71
86
|
```
|
|
72
87
|
|
|
73
|
-
The zero-dependency default reads the same, minus the `mockFactory
|
|
74
|
-
|
|
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()`:
|
|
75
90
|
|
|
76
|
-
```
|
|
91
|
+
```ts
|
|
77
92
|
import { TestBed } from "@codefast/di-testing";
|
|
78
93
|
import assert from "node:assert/strict";
|
|
79
94
|
|
|
@@ -82,81 +97,143 @@ unit.placeOrder("u1", 42);
|
|
|
82
97
|
assert.deepEqual(mocks.get(PaymentGatewayToken).charge.mock.calls[0], ["u1", 42]);
|
|
83
98
|
```
|
|
84
99
|
|
|
85
|
-
##
|
|
100
|
+
## Solitary beds
|
|
86
101
|
|
|
87
|
-
|
|
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:
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- `mockFactory?: () => spy` — the spy backend for each auto-mock. Defaults to the built-in zero-dependency spy.
|
|
105
|
+
- `mockFactory?: () => spy` — the spy backend each auto-mock is built from. Defaults to the built-in spy.
|
|
92
106
|
- `metadataReader?: MetadataReader` — the reader dependencies are discovered through. Defaults to di's reader.
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
The builder records overrides, then compiles:
|
|
95
109
|
|
|
96
110
|
- `.mock(token).stub((fn) => stub)` — bind a partial stub built from the active spy factory; unlisted members stay
|
|
97
111
|
auto-mocked. `fn()` is typed as whatever the backend produces, so `fn().mockReturnValue(...)` (jest-shaped) or
|
|
98
|
-
`fn().returns(...)` (Sinon)
|
|
112
|
+
`fn().returns(...)` (Sinon) type-check against the factory you chose.
|
|
99
113
|
- `.mock(token).using(value)` — bind a fixed value. The value is **sealed**: it has no mock surface, so `mocks.get`
|
|
100
114
|
refuses it rather than hand it back mistyped — the test already holds the reference it passed in.
|
|
101
115
|
- `.mock(token).absent()` — leave the dependency unbound: an `optional()` slot resolves `undefined`, an `injectAll()`
|
|
102
|
-
slot `[]`.
|
|
116
|
+
slot `[]`. On a required dependency this is an `OverrideMismatchError`.
|
|
103
117
|
- `.mock(token).usingAll([a, b])` — supply the elements of an unconstrained `injectAll()` slot, in order. Sealed like
|
|
104
118
|
`.using`.
|
|
105
119
|
- `.mock(token, { name })` / `.mock(token, { tag })` — target one slot of a token that is injected several ways; the
|
|
106
|
-
slotless form covers every slot without a more specific override.
|
|
107
|
-
|
|
108
|
-
- `.
|
|
120
|
+
slotless form covers every slot without a more specific override. Registering the same target twice replaces the
|
|
121
|
+
earlier override.
|
|
122
|
+
- `.compile()` — instantiate the unit synchronously, running accessor injection and `@postConstruct`.
|
|
123
|
+
- `.compileAsync()` — the same for a unit whose `@postConstruct` is asynchronous.
|
|
124
|
+
|
|
125
|
+
`.mock(...)` accepts a token or a class, matching how the unit declares the dependency.
|
|
109
126
|
|
|
110
|
-
|
|
127
|
+
## Sociable beds
|
|
111
128
|
|
|
112
129
|
A sociable bed keeps chosen collaborators real while everything else stays mocked — a unit test over a small real
|
|
113
|
-
subtree, not an integration test.
|
|
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.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { injectable, token } from "@codefast/di";
|
|
135
|
+
import { TestBed } from "@codefast/di-testing";
|
|
136
|
+
import { expect, it, vi } from "vitest";
|
|
137
|
+
|
|
138
|
+
interface TaxPolicy {
|
|
139
|
+
rateFor(currency: string): number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const TaxPolicyToken = token<TaxPolicy>("TaxPolicy");
|
|
143
|
+
|
|
144
|
+
@injectable([TaxPolicyToken])
|
|
145
|
+
class PricingService {
|
|
146
|
+
constructor(private readonly tax: TaxPolicy) {}
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
total(amount: number, currency: string): number {
|
|
149
|
+
return amount * (1 + this.tax.rateFor(currency));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@injectable([PricingService])
|
|
154
|
+
class CheckoutService {
|
|
155
|
+
constructor(private readonly pricing: PricingService) {}
|
|
156
|
+
|
|
157
|
+
checkout(amount: number, currency: string): number {
|
|
158
|
+
return this.pricing.total(amount, currency);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
it("prices through the real PricingService over a mocked tax boundary", () => {
|
|
163
|
+
const bed = TestBed.sociable(CheckoutService, { mockFactory: () => vi.fn() })
|
|
164
|
+
.expose(PricingService)
|
|
165
|
+
.mock(TaxPolicyToken)
|
|
166
|
+
.stub((fn) => ({ rateFor: fn().mockReturnValue(0.1) }))
|
|
167
|
+
.compile();
|
|
121
168
|
|
|
122
|
-
bed.unit.checkout(100, "USD");
|
|
123
|
-
bed.exposed(PricingService);
|
|
169
|
+
expect(bed.unit.checkout(100, "USD")).toBe(110);
|
|
170
|
+
expect(bed.exposed(PricingService)).toBeInstanceOf(PricingService);
|
|
171
|
+
});
|
|
124
172
|
```
|
|
125
173
|
|
|
126
174
|
- **Exposure follows class identity.** A class-keyed dependency — of the unit or of another exposed class — stays real
|
|
127
175
|
when exposed, and its own dependencies follow the same rules recursively.
|
|
128
176
|
- **Tokens are the boundary.** A `Token`-keyed dependency is always mocked, in both modes: tokens mark where the logic
|
|
129
177
|
under test meets the outside world.
|
|
130
|
-
- Exposed collaborators are
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- Exposing a class the unit never reaches
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
- Exposed collaborators are singletons resolved through the container, so their `@postConstruct` runs at compile and
|
|
179
|
+
`@preDestroy` on dispose. `bed.exposed(Class)` returns the instance the unit was built with; `mocks.get(Class)`
|
|
180
|
+
refuses it with a `SealedDependencyError` because it carries no mock surface.
|
|
181
|
+
- Exposing the unit itself, or a class the unit never reaches through exposed collaborators, is an `ExposureError` at
|
|
182
|
+
compile.
|
|
183
|
+
|
|
184
|
+
## Behaviour notes
|
|
185
|
+
|
|
186
|
+
- A class whose constructor takes no parameters is testable without `@injectable`; a parameterful constructor with no
|
|
187
|
+
metadata is a `NotInjectableError`.
|
|
188
|
+
- An `optional()` dependency is auto-mocked like any other — it resolves to the mock, not to the `undefined` an unbound
|
|
189
|
+
optional would give in production. Use `.mock(token).absent()` to exercise the absent branch.
|
|
139
190
|
- An `injectAll()` dependency receives a one-element array holding the token's mock; use `.mock(token).usingAll([...])`
|
|
140
|
-
to supply several elements.
|
|
141
|
-
the unconstrained `injectAll` too — di's `resolveAll` takes every binding of the token.
|
|
191
|
+
to supply several elements.
|
|
142
192
|
- Named or tagged parameters of one token share the token's mock unless a slot-targeted `.mock(token, { name })` gives
|
|
143
193
|
that slot its own; either way `mocks.get(token, { name })` addresses the slot directly.
|
|
194
|
+
- A `.mock(...)` that names a token or slot the unit does not declare is an `UndeclaredDependencyError` at compile — a
|
|
195
|
+
typo fails loudly instead of binding an unused constant.
|
|
196
|
+
- A failed compile disposes the container it was building, so no lifecycle state leaks between tests.
|
|
197
|
+
|
|
198
|
+
## Result
|
|
144
199
|
|
|
145
|
-
|
|
200
|
+
`compile()` returns a `UnitTestBed` (a sociable bed returns a `SociableUnitTestBed`, which adds `exposed`):
|
|
146
201
|
|
|
147
202
|
- `unit` — the real class under test.
|
|
148
|
-
- `mocks.get(token, options?)` — the `Mocked<T>` bound for a dependency
|
|
149
|
-
stubs come back; sealed values throw `SealedDependencyError`.
|
|
150
|
-
- `resetMocks()` — clear the call history and configured behaviour of every auto-mock the bed created.
|
|
151
|
-
- `dispose()` — run the unit's `@preDestroy` hooks and dispose the container.
|
|
203
|
+
- `mocks.get(token, options?)` — the `Mocked<T>` bound for a dependency, or for one slot of it. Only auto-mocks and
|
|
204
|
+
`.stub` stubs come back; sealed values and exposed classes throw `SealedDependencyError`.
|
|
205
|
+
- `resetMocks()` — clear the call history and configured behaviour of every auto-mock and stub the bed created.
|
|
206
|
+
- `dispose()` — run the unit's `@preDestroy` hooks and dispose the container.
|
|
207
|
+
|
|
208
|
+
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.
|
|
210
|
+
|
|
211
|
+
The lower-level pieces are exported too: `createAutoMock`, `createSpy`, `defaultMockFactory`, and the `Mocked`,
|
|
212
|
+
`DeepPartial`, `MockFactory`, and `Spy` types.
|
|
152
213
|
|
|
153
214
|
## Errors
|
|
154
215
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
216
|
+
Every error extends `TestingError` and carries a stable `code`. `TestingError` mirrors di's `DiError` shape without
|
|
217
|
+
extending it, so setup failures can be caught separately from resolution failures.
|
|
218
|
+
|
|
219
|
+
| Error | Code | Raised when |
|
|
220
|
+
| --------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------- |
|
|
221
|
+
| `NotInjectableError` | `NOT_INJECTABLE` | A scanned class takes constructor parameters but is not `@injectable` |
|
|
222
|
+
| `UndeclaredDependencyError` | `UNDECLARED_DEPENDENCY` | A `.mock(...)` or `mocks.get(...)` named a token or slot the unit does not use |
|
|
223
|
+
| `SealedDependencyError` | `SEALED_DEPENDENCY` | `mocks.get(...)` asked for a `.using()`/`.absent()`/`.usingAll()` value or an exposed class |
|
|
224
|
+
| `OverrideMismatchError` | `OVERRIDE_MISMATCH` | `.absent()` on a required dependency, or `.usingAll()` with no unconstrained `injectAll()` slot |
|
|
225
|
+
| `ExposureError` | `EXPOSURE` | An exposed class is the unit, is unreachable, or `bed.exposed()` names a class that was not exposed |
|
|
226
|
+
|
|
227
|
+
## Documentation
|
|
228
|
+
|
|
229
|
+
- [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`.
|
|
231
|
+
- [`CHANGELOG.md`](./CHANGELOG.md) — release history.
|
|
232
|
+
|
|
233
|
+
## Contributing
|
|
234
|
+
|
|
235
|
+
See the repo-wide [contributing guide](../../CONTRIBUTING.md) for setup, conventions, and the test taxonomy.
|
|
159
236
|
|
|
160
237
|
## License
|
|
161
238
|
|
|
162
|
-
MIT
|
|
239
|
+
Released under the [MIT License](./LICENSE).
|
|
@@ -31,5 +31,4 @@ export interface SociableScan {
|
|
|
31
31
|
*
|
|
32
32
|
* @since 0.1.0
|
|
33
33
|
*/
|
|
34
|
-
export declare function scanSociableDependencies(target: Constructor, exposed: ReadonlySet<Constructor>, reader: MetadataReader): SociableScan;
|
|
35
|
-
//# sourceMappingURL=dependency-scanner.d.ts.map
|
|
34
|
+
export declare function scanSociableDependencies(target: Constructor, exposed: ReadonlySet<Constructor>, reader: MetadataReader): SociableScan;
|
|
@@ -60,7 +60,8 @@ export interface BoundMock {
|
|
|
60
60
|
* Folds an `InjectOptions` into slot criteria, or `undefined` when it names no criterion.
|
|
61
61
|
*
|
|
62
62
|
* @remarks Tags are copied and deduplicated by identity — di interns each `(key, value)` pair, so a
|
|
63
|
-
* repeated pair is the same object.
|
|
63
|
+
* repeated pair is the same object. A reserved `slotName` criterion folds into `name`, so both
|
|
64
|
+
* spellings address one slot.
|
|
64
65
|
*
|
|
65
66
|
* @since 0.1.0
|
|
66
67
|
*/
|
|
@@ -83,5 +84,4 @@ export declare function criteriaEquals(left: SlotCriteria, right: SlotCriteria):
|
|
|
83
84
|
*
|
|
84
85
|
* @since 0.1.0
|
|
85
86
|
*/
|
|
86
|
-
export declare function bindMocks(container: Container, dependencies: ReadonlyArray<DependencySlot>, overrides: ReadonlyMap<DependencyKey, ReadonlyArray<SlottedOverride>>, mockFactory: MockFactory, exposed?: ReadonlySet<Constructor>): ReadonlyMap<DependencyKey, ReadonlyArray<BoundMock>>;
|
|
87
|
-
//# sourceMappingURL=mock-binder.d.ts.map
|
|
87
|
+
export declare function bindMocks(container: Container, dependencies: ReadonlyArray<DependencySlot>, overrides: ReadonlyMap<DependencyKey, ReadonlyArray<SlottedOverride>>, mockFactory: MockFactory, exposed?: ReadonlySet<Constructor>): ReadonlyMap<DependencyKey, ReadonlyArray<BoundMock>>;
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
/** Binds a unit's discovered dependencies onto a container as mocks — the sole container coupling. */
|
|
2
|
-
import { tokenName } from "@codefast/di";
|
|
2
|
+
import { slotName, tokenName } from "@codefast/di";
|
|
3
3
|
import { OverrideMismatchError, UndeclaredDependencyError } from "#/errors/errors";
|
|
4
4
|
import { createAutoMock } from "#/mocking/auto-mock";
|
|
5
5
|
/** The name prefix `usingAll` elements are bound under, so identical constants keep distinct slots. */
|
|
6
6
|
const ALL_ELEMENT_SLOT_PREFIX = "di-testing:all:";
|
|
7
|
+
/**
|
|
8
|
+
* One name and plain tag set, with di's reserved criterion folded into the name.
|
|
9
|
+
*
|
|
10
|
+
* @remarks di treats `{name: "x"}` and `{tag: slotName.of("x")}` as one slot, so addressing here
|
|
11
|
+
* must too — mirroring `bindingSlotToResolveOptions` in di.
|
|
12
|
+
*/
|
|
13
|
+
function foldReservedName(name, tags) {
|
|
14
|
+
const reserved = tags.find((tag) => tag.key === slotName);
|
|
15
|
+
if (reserved === undefined) {
|
|
16
|
+
return { name, tags };
|
|
17
|
+
}
|
|
18
|
+
return { name: name ?? reserved.value, tags: tags.filter((tag) => tag.key !== slotName) };
|
|
19
|
+
}
|
|
7
20
|
/**
|
|
8
21
|
* Folds an `InjectOptions` into slot criteria, or `undefined` when it names no criterion.
|
|
9
22
|
*
|
|
10
23
|
* @remarks Tags are copied and deduplicated by identity — di interns each `(key, value)` pair, so a
|
|
11
|
-
* repeated pair is the same object.
|
|
24
|
+
* repeated pair is the same object. A reserved `slotName` criterion folds into `name`, so both
|
|
25
|
+
* spellings address one slot.
|
|
12
26
|
*
|
|
13
27
|
* @since 0.1.0
|
|
14
28
|
*/
|
|
@@ -16,12 +30,12 @@ export function normalizeCriteria(options) {
|
|
|
16
30
|
if (options === undefined) {
|
|
17
31
|
return undefined;
|
|
18
32
|
}
|
|
19
|
-
const
|
|
20
|
-
const tags = [...new Set(
|
|
21
|
-
if (
|
|
33
|
+
const merged = options.tag === undefined ? (options.tags ?? []) : [options.tag, ...(options.tags ?? [])];
|
|
34
|
+
const { name, tags } = foldReservedName(options.name, [...new Set(merged)]);
|
|
35
|
+
if (name === undefined && tags.length === 0) {
|
|
22
36
|
return undefined;
|
|
23
37
|
}
|
|
24
|
-
return { name
|
|
38
|
+
return { name, tags };
|
|
25
39
|
}
|
|
26
40
|
/**
|
|
27
41
|
* Returns whether two criteria address the same slot — equal name and the same tag set.
|
|
@@ -47,15 +61,16 @@ function tagSetEquals(left, right) {
|
|
|
47
61
|
}
|
|
48
62
|
/** Returns the slot's own criteria, or `undefined` for an unconstrained slot. */
|
|
49
63
|
function criteriaOfSlot(slot) {
|
|
50
|
-
const tags = slot.tags ?? [];
|
|
51
|
-
if (
|
|
64
|
+
const { name, tags } = foldReservedName(slot.name, slot.tags ?? []);
|
|
65
|
+
if (name === undefined && tags.length === 0) {
|
|
52
66
|
return undefined;
|
|
53
67
|
}
|
|
54
|
-
return { name
|
|
68
|
+
return { name, tags };
|
|
55
69
|
}
|
|
56
70
|
/** Returns whether an override's criteria address exactly this slot. */
|
|
57
71
|
function matchesSlot(criteria, slot) {
|
|
58
|
-
|
|
72
|
+
const folded = foldReservedName(slot.name, slot.tags ?? []);
|
|
73
|
+
return criteria.name === folded.name && tagSetEquals(criteria.tags, folded.tags);
|
|
59
74
|
}
|
|
60
75
|
/** A readable rendering of criteria for error messages. */
|
|
61
76
|
function describeCriteria(criteria) {
|
|
@@ -208,5 +223,4 @@ function buildMock(override, mockFactory) {
|
|
|
208
223
|
}
|
|
209
224
|
// The stub's seed is built here, once per compile, so beds never share seeded spies.
|
|
210
225
|
return createAutoMock(mockFactory, override.setup(mockFactory));
|
|
211
|
-
}
|
|
212
|
-
//# sourceMappingURL=mock-binder.js.map
|
|
226
|
+
}
|
package/dist/errors/errors.d.ts
CHANGED
package/dist/errors/errors.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -13,5 +13,4 @@ export { createSpy } from "#/mocking/spy";
|
|
|
13
13
|
export type { Spy, SpyResult, SpyState } from "#/mocking/spy";
|
|
14
14
|
export type { InjectionIdentifier } from "#/types";
|
|
15
15
|
export { ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#/errors/errors";
|
|
16
|
-
export type { SealedCause } from "#/errors/errors";
|
|
17
|
-
//# sourceMappingURL=index.d.ts.map
|
|
16
|
+
export type { SealedCause } from "#/errors/errors";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,4 @@ export { 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";
|
|
7
|
-
//# sourceMappingURL=index.js.map
|
|
6
|
+
export { ExposureError, NotInjectableError, OverrideMismatchError, SealedDependencyError, TestingError, UndeclaredDependencyError, } from "#/errors/errors";
|
|
@@ -51,5 +51,4 @@ export declare const MOCK_RESET: unique symbol;
|
|
|
51
51
|
*
|
|
52
52
|
* @since 0.1.0
|
|
53
53
|
*/
|
|
54
|
-
export declare function createAutoMock<Dependency, Backend extends MockFunction = Spy>(mockFactory: MockFactory<Backend>, seed?: DeepPartial<Dependency>): Mocked<Dependency, Backend>;
|
|
55
|
-
//# sourceMappingURL=auto-mock.d.ts.map
|
|
54
|
+
export declare function createAutoMock<Dependency, Backend extends MockFunction = Spy>(mockFactory: MockFactory<Backend>, seed?: DeepPartial<Dependency>): Mocked<Dependency, Backend>;
|
|
@@ -27,5 +27,4 @@ export type MockFactory<Backend extends MockFunction = MockFunction> = () => Bac
|
|
|
27
27
|
*
|
|
28
28
|
* @since 0.1.0
|
|
29
29
|
*/
|
|
30
|
-
export declare const defaultMockFactory: MockFactory<Spy>;
|
|
31
|
-
//# sourceMappingURL=mock-factory.d.ts.map
|
|
30
|
+
export declare const defaultMockFactory: MockFactory<Spy>;
|
package/dist/mocking/spy.d.ts
CHANGED
package/dist/mocking/spy.js
CHANGED
|
@@ -87,5 +87,4 @@ export declare abstract class BedBuilder<Class, Backend extends MockFunction = S
|
|
|
87
87
|
protected compileWith<Prepared extends PreparedBed, Bed>(prepare: () => Prepared, build: (unit: Class, prepared: Prepared) => Bed): Bed;
|
|
88
88
|
/** The async twin of {@link compileWith}, for units whose activation is asynchronous. */
|
|
89
89
|
protected compileWithAsync<Prepared extends PreparedBed, Bed>(prepare: () => Prepared, build: (unit: Class, prepared: Prepared) => Bed): Promise<Bed>;
|
|
90
|
-
}
|
|
91
|
-
//# sourceMappingURL=bed-builder.d.ts.map
|
|
90
|
+
}
|
|
@@ -41,5 +41,4 @@ export declare class SociableBuilder<Class, Backend extends MockFunction = Spy>
|
|
|
41
41
|
expose(target: Constructor): this;
|
|
42
42
|
compile(): SociableUnitTestBed<Class, Backend>;
|
|
43
43
|
compileAsync(): Promise<SociableUnitTestBed<Class, Backend>>;
|
|
44
|
-
}
|
|
45
|
-
//# sourceMappingURL=sociable-builder.d.ts.map
|
|
44
|
+
}
|
|
@@ -71,5 +71,4 @@ export declare function createSociableUnitTestBed<Class, Backend extends MockFun
|
|
|
71
71
|
*
|
|
72
72
|
* @since 0.1.0
|
|
73
73
|
*/
|
|
74
|
-
export declare function createUnitTestBed<Class, Backend extends MockFunction = Spy>(unit: Class, entries: ReadonlyMap<DependencyKey, ReadonlyArray<BoundMock>>, container: Container): UnitTestBed<Class, Backend>;
|
|
75
|
-
//# sourceMappingURL=unit-test-bed.d.ts.map
|
|
74
|
+
export declare function createUnitTestBed<Class, Backend extends MockFunction = Spy>(unit: Class, entries: ReadonlyMap<DependencyKey, ReadonlyArray<BoundMock>>, container: Container): UnitTestBed<Class, Backend>;
|
package/dist/types.d.ts
CHANGED
|
@@ -10,5 +10,4 @@ import type { Constructor, Token } from "@codefast/di";
|
|
|
10
10
|
*
|
|
11
11
|
* @since 0.1.0
|
|
12
12
|
*/
|
|
13
|
-
export type InjectionIdentifier<Value> = Token<Value> | Constructor<Value>;
|
|
14
|
-
//# sourceMappingURL=types.d.ts.map
|
|
13
|
+
export type InjectionIdentifier<Value> = Token<Value> | Constructor<Value>;
|
package/dist/types.js
CHANGED