@dunx/testing 2.5.0 → 3.0.1
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/README.md +32 -135
- package/dist/index.js +1 -1
- package/dist/server.d.ts +9 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# @dunx/testing
|
|
2
2
|
|
|
3
|
-
The container an app already has, with named bindings **replaced in place**, plus
|
|
4
|
-
real `Bun.serve` on port 0.
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The container an app already has, with named bindings **replaced in place**, plus
|
|
4
|
+
a real `Bun.serve` on port 0. Bun binds a socket in about a millisecond, so the
|
|
5
|
+
thing under test is the thing that ships: there is no mocking framework, no fake
|
|
6
|
+
request object and no in-memory transport.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add -d @dunx/testing
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
7
15
|
|
|
8
16
|
```ts
|
|
9
17
|
import { provide } from '@dunx/core';
|
|
@@ -27,140 +35,29 @@ const { status, body } = await server.json<User[]>('api/users');
|
|
|
27
35
|
await server.close();
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
This is the whole design, and it follows from how `@dunx/core` applies an override:
|
|
33
|
-
by **substitution into the binding that already exists**, never by appending a
|
|
34
|
-
registration that has to out-rank the real one.
|
|
35
|
-
|
|
36
|
-
A module is a scope, and `providers` are private to it. So a test override cannot be
|
|
37
|
-
an extra module appended at the end that wins - an appended module's providers are
|
|
38
|
-
invisible to every scope that does not import it, being the scope the
|
|
39
|
-
code under test resolves from.
|
|
40
|
-
|
|
41
|
-
`createTestApp` therefore builds the same scope graph the app would have and
|
|
42
|
-
substitutes by token inside it. Three consequences worth relying on:
|
|
43
|
-
|
|
44
|
-
- **An override replaces the binding in every scope that holds it.** A test stubbing
|
|
45
|
-
`Logger` does not have to know how many modules bind it, and does not have to name
|
|
46
|
-
a scope. Where two scopes genuinely bind one token differently and only one is
|
|
47
|
-
meant, resolve through the module you care about instead.
|
|
48
|
-
- **An override naming a token nobody binds is an error** rather than a silent no-op. A
|
|
49
|
-
typo'd token would otherwise leave the suite asserting against the real provider
|
|
50
|
-
it thought it had swapped, the failure mode this package exists to
|
|
51
|
-
prevent.
|
|
52
|
-
- **The discarded provider is never instantiated.** Its `useFactory` never runs and
|
|
53
|
-
its `onInit` never fires. Overriding the database does not open a connection to
|
|
54
|
-
the real database - that is the one guarantee here that a hand-rolled fixture
|
|
55
|
-
usually gets wrong, and `app.test.ts` proves it with a factory that throws if it
|
|
56
|
-
is ever called.
|
|
57
|
-
|
|
58
|
-
`Logger` and `RequestContext` are overridable too, even though no module binds
|
|
59
|
-
them: core offers a default for each after every module, and the substitution
|
|
60
|
-
applies there as well.
|
|
61
|
-
|
|
62
|
-
## API
|
|
63
|
-
|
|
64
|
-
| Export | What it is |
|
|
65
|
-
| --------------------------- | ----------------------------------------------------------------------- |
|
|
66
|
-
| `createTestApp(options)` | `Promise<App>` - the core container, overrides applied |
|
|
67
|
-
| `createTestServer(options)` | `Promise<TestServer>` - the same, plus `Bun.serve` on port 0 and a client |
|
|
68
|
-
| `testClient(url)` | `TestClient` - the request helpers against any base URL |
|
|
69
|
-
| `testRoot(modules)` | The synthetic root module, for driving `HttpFactory` yourself |
|
|
70
|
-
| `RecordingLogger` | A `Logger` that keeps entries instead of writing them |
|
|
71
|
-
|
|
72
|
-
`modules` takes one module ref or several; several become the `imports` of one
|
|
73
|
-
synthetic root, so no fixture module has to be written by hand. Anything a module
|
|
74
|
-
ref can be works - a class, or a `DynamicModule` from a `forRoot`.
|
|
75
|
-
|
|
76
|
-
`TestServer` is a `TestClient` plus `app` (the real `HttpApp`, for
|
|
77
|
-
`app.get(...)`) and `close()`.
|
|
78
|
-
|
|
79
|
-
`createTestServer` passes `HttpOptions` through, with two differences: `port`
|
|
80
|
-
is always 0, and **`requestLogging` defaults to `false`** - it is on by default
|
|
81
|
-
in production for good reasons, none of which apply to a suite that would
|
|
82
|
-
otherwise print one JSON line per assertion. Pass `requestLogging: true` to
|
|
83
|
-
test the logging itself.
|
|
84
|
-
|
|
85
|
-
Everything else is **absent unless passed**, and `middleware` and `onError`
|
|
86
|
-
decide what the application is: forget them and the fixture has no global
|
|
87
|
-
guards and the default error mapper, and answers 200 where production answers
|
|
88
|
-
401. Export one `httpOptions(config)` and spread it into both `main.ts` and
|
|
89
|
-
every suite.
|
|
90
|
-
|
|
91
|
-
Omitting `middleware` in a graph that declares a `Middleware` no `@UseGuards`
|
|
92
|
-
attaches warns on `console.warn`; `middleware: []` says the omission is
|
|
93
|
-
deliberate.
|
|
94
|
-
|
|
95
|
-
### The client
|
|
96
|
-
|
|
97
|
-
Two methods, because a third would be the start of an assertion DSL:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const { status, headers, body } = await server.json<Page>('notes?limit=10');
|
|
101
|
-
await server.json('notes/7', { method: 'PATCH', json: { title: 'edited' } });
|
|
102
|
-
const image = await server.request('avatars/7.png'); // the raw Response
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
`json` on the init object is serialized and sets `content-type: application/json`
|
|
106
|
-
unless `headers` already carries one - one option for every verb, rather than a
|
|
107
|
-
`post`/`put`/`patch` triple. `json()` reads the body as text before parsing, so a
|
|
108
|
-
route that answered 204, HTML or a plain-text error fails with the status,
|
|
109
|
-
content-type and body rather than with `JSON.parse`'s message.
|
|
38
|
+
## What is here
|
|
110
39
|
|
|
111
|
-
|
|
40
|
+
The [Testing guide](../../docs/guide/11-testing.md) is canonical.
|
|
112
41
|
|
|
113
|
-
|
|
114
|
-
|
|
42
|
+
| Export | What it does |
|
|
43
|
+
| ------------------ | ---------------------------------------------------------------- |
|
|
44
|
+
| `createTestApp` | The container, with overrides applied before anything resolves |
|
|
45
|
+
| `createTestServer` | The same, behind a real `Bun.serve` on port 0 |
|
|
46
|
+
| `testClient` | The fetch-and-parse plumbing against a base url |
|
|
115
47
|
|
|
116
|
-
|
|
117
|
-
const logger = new RecordingLogger();
|
|
118
|
-
await createTestApp({
|
|
119
|
-
modules: [PaymentsModule],
|
|
120
|
-
overrides: [provide(Logger, { useValue: logger })],
|
|
121
|
-
});
|
|
122
|
-
expect(logger.at(LogLevel.ERROR)).toEqual([]);
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
It records; it does not interpret. No level filtering, no error promotion, no
|
|
126
|
-
merging of extras - those are `@arkv/logger`'s behaviour, and asserting against a
|
|
127
|
-
reimplementation of them would prove nothing.
|
|
128
|
-
|
|
129
|
-
## Not here
|
|
130
|
-
|
|
131
|
-
- **A fluent assertion DSL** (`expect(res).toHaveStatus(200)`, supertest-style
|
|
132
|
-
chaining). `status` and a parsed `body` read fine through `expect` already, and a
|
|
133
|
-
matcher library would be a second vocabulary to learn for no new capability.
|
|
134
|
-
- **Provider spies / partial mocks.** `provide(Token, { useValue })` with a class
|
|
135
|
-
the test wrote is smaller than any mocking API, and `bun test` already ships
|
|
136
|
-
`mock()` and `spyOn()` for a method on an instance the container handed back.
|
|
137
|
-
- **A `providers` key on the options.** `{ modules, overrides }` is the shape, on
|
|
138
|
-
purpose: a suite tests the modules an app actually ships. A fixture class that
|
|
139
|
-
needs binding goes in a two-line `@Module`, which is also where it would live if
|
|
140
|
-
it were real.
|
|
141
|
-
- **A fake HTTP dispatcher.** It could only exercise the parts of the request path
|
|
142
|
-
dunx wrote, and not the parts Bun owns - route matching, params, method
|
|
143
|
-
dispatch, upgrades. The real server is cheaper than the lie.
|
|
144
|
-
- **Database fixtures, transactional rollback, seeding.** That is drizzle's
|
|
145
|
-
surface rather than this package's. `@dunx/infra/db` binds an in-memory `bun:sqlite`
|
|
146
|
-
with the same driver as production, which is a better fixture than a mock.
|
|
147
|
-
- **A websocket client.** Bun implements `WebSocket` natively, and a gateway test
|
|
148
|
-
is `new WebSocket(server.url.replace('http', 'ws') + '/chat')`. Wrapping that
|
|
149
|
-
would add nothing.
|
|
48
|
+
## Notes
|
|
150
49
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
50
|
+
- An override replaces the binding in **every scope that holds it**, so a test
|
|
51
|
+
stubbing `Logger` need not know how many modules bind it. Naming a token
|
|
52
|
+
nobody binds is an error rather than a silent no-op.
|
|
53
|
+
- The replacement happens before anything resolves, so the discarded provider is
|
|
54
|
+
never constructed: its `useFactory` never runs and its `onInit` never fires,
|
|
55
|
+
which makes overriding a database safe.
|
|
56
|
+
- Request logging and boot logging are off unless asked for.
|
|
57
|
+
- An `HttpOptions` field not passed is absent; nothing is inherited from
|
|
58
|
+
production. `middleware` and `onError` change what the application does, so
|
|
59
|
+
pass the same object `main.ts` passes.
|
|
156
60
|
|
|
157
|
-
|
|
158
|
-
is that your app and this package resolve to **one copy of `@dunx/core`** - two
|
|
159
|
-
copies means two `Logger` classes and two `RequestContext` classes, so tokens that
|
|
160
|
-
match nothing and overrides that silently replace nothing. A caret range hoists to
|
|
161
|
-
the copy your app already has.
|
|
61
|
+
## License
|
|
162
62
|
|
|
163
|
-
|
|
164
|
-
derives its build order from `dependencies` only, so a peer-only manifest cannot be
|
|
165
|
-
built in this monorepo at all. The reasoning and the measurement are in
|
|
166
|
-
[architecture/packaging.md](../../docs/architecture/packaging.md), "Test harness".
|
|
63
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -90,9 +90,9 @@ import {
|
|
|
90
90
|
readControllers
|
|
91
91
|
} from "@dunx/core";
|
|
92
92
|
import {
|
|
93
|
-
discoverRoutes,
|
|
94
93
|
HttpFactory
|
|
95
94
|
} from "@dunx/http";
|
|
95
|
+
import { discoverRoutes } from "@dunx/http/internal";
|
|
96
96
|
var middlewareShaped = (ctor) => typeof ctor.prototype?.handle === "function";
|
|
97
97
|
var declaredMiddleware = (modules) => {
|
|
98
98
|
const found = [];
|
package/dist/server.d.ts
CHANGED
|
@@ -18,11 +18,10 @@ export interface TestServer extends TestClient {
|
|
|
18
18
|
close(): Promise<void>;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* A
|
|
22
|
-
* {@link createTestApp}.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* dispatch, upgrades.
|
|
21
|
+
* A real `Bun.serve` on port 0, with the same override semantics as
|
|
22
|
+
* {@link createTestApp}. `Bun.serve` binds in about a millisecond, and a fake
|
|
23
|
+
* could only prove the parts of the request path dunx wrote rather than the parts
|
|
24
|
+
* Bun owns - routing, params, method dispatch, upgrades.
|
|
26
25
|
*
|
|
27
26
|
* ```ts
|
|
28
27
|
* const server = await createTestServer({ modules: [ApiModule], prefix: 'api' });
|
|
@@ -30,15 +29,11 @@ export interface TestServer extends TestClient {
|
|
|
30
29
|
* await server.close();
|
|
31
30
|
* ```
|
|
32
31
|
*
|
|
33
|
-
* Request logging and boot logging are
|
|
34
|
-
*
|
|
35
|
-
* print one JSON line per assertion and one route table per file.
|
|
32
|
+
* Request logging and boot logging are off unless asked for, since a suite would
|
|
33
|
+
* otherwise print one JSON line per assertion and one route table per file.
|
|
36
34
|
*
|
|
37
|
-
*
|
|
38
|
-
* `middleware`
|
|
39
|
-
*
|
|
40
|
-
* exported `httpOptions(config)` spread into both. Omitting `middleware` in a graph
|
|
41
|
-
* that declares a `Middleware` no `@UseGuards` attaches writes one line to
|
|
42
|
-
* `console.warn`; `middleware: []` says the omission is deliberate.
|
|
35
|
+
* An `HttpOptions` field not passed is absent, not inherited from production.
|
|
36
|
+
* `middleware` and `onError` change what the application does, so pass the same
|
|
37
|
+
* object `main.ts` passes. `middleware: []` says the omission is deliberate.
|
|
43
38
|
*/
|
|
44
39
|
export declare const createTestServer: (options: TestServerOptions) => Promise<TestServer>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunx/testing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Test harness for dunx apps: a container with providers replaced in place, and a real Bun.serve on port 0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@dunx/http": "workspace:*"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@dunx/core": "^
|
|
55
|
-
"@dunx/http": "^
|
|
54
|
+
"@dunx/core": "^3.0.1",
|
|
55
|
+
"@dunx/http": "^3.0.1",
|
|
56
56
|
"@types/bun": ">=1.3.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|