@jterrazz/test 3.4.0 → 3.5.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/README.md +245 -73
- package/dist/index.cjs +207 -4394
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +62 -4271
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/dist/assets/cpufeatures-B-FC6C9Z.node +0 -0
- package/dist/assets/sshcrypto-D82met6T.node +0 -0
- package/dist/build.cjs +0 -122200
- package/dist/build.cjs.map +0 -1
- package/dist/build.js +0 -122193
- package/dist/build.js.map +0 -1
- package/dist/chunk.cjs +0 -64
- package/dist/chunk.js +0 -37
- package/dist/dist.cjs +0 -6587
- package/dist/dist.cjs.map +0 -1
- package/dist/dist.js +0 -6582
- package/dist/dist.js.map +0 -1
- package/dist/dist2.cjs +0 -20838
- package/dist/dist2.cjs.map +0 -1
- package/dist/dist2.js +0 -20834
- package/dist/dist2.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
# @jterrazz/test
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
3
|
+
Declarative testing framework for APIs and CLIs. Same fluent builder API, three execution modes.
|
|
6
4
|
|
|
7
5
|
```bash
|
|
8
6
|
npm install -D @jterrazz/test vitest
|
|
9
7
|
```
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Specification runners
|
|
14
|
-
|
|
15
|
-
Declare services, provide an app factory, the framework starts containers and wires everything.
|
|
9
|
+
## Quick start
|
|
16
10
|
|
|
17
|
-
###
|
|
11
|
+
### API testing (HTTP)
|
|
18
12
|
|
|
19
13
|
```typescript
|
|
20
|
-
// tests/
|
|
14
|
+
// tests/setup/integration.specification.ts
|
|
21
15
|
import { afterAll } from "vitest";
|
|
22
16
|
import { integration, postgres } from "@jterrazz/test";
|
|
23
17
|
import { createApp } from "../../src/app.js";
|
|
@@ -33,37 +27,198 @@ export const spec = await integration({
|
|
|
33
27
|
afterAll(() => spec.cleanup());
|
|
34
28
|
```
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
```typescript
|
|
31
|
+
// tests/e2e/users/users.e2e.test.ts
|
|
32
|
+
import { spec } from "../../setup/integration.specification.js";
|
|
33
|
+
|
|
34
|
+
test("creates a user", async () => {
|
|
35
|
+
// Given — one existing user
|
|
36
|
+
const result = await spec("creates user")
|
|
37
|
+
.seed("initial-users.sql")
|
|
38
|
+
.post("/users", "new-user.json")
|
|
39
|
+
.run();
|
|
40
|
+
|
|
41
|
+
// Then — user created
|
|
42
|
+
result.expectStatus(201);
|
|
43
|
+
await result.expectTable("users", {
|
|
44
|
+
columns: ["name"],
|
|
45
|
+
rows: [["Alice"], ["Bob"]],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### CLI testing
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// tests/setup/cli.specification.ts
|
|
54
|
+
import { resolve } from "node:path";
|
|
55
|
+
import { cli } from "@jterrazz/test";
|
|
56
|
+
|
|
57
|
+
export const spec = await cli({
|
|
58
|
+
command: resolve(import.meta.dirname, "../../bin/my-cli.sh"),
|
|
59
|
+
root: "../fixtures",
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// tests/e2e/build/build.e2e.test.ts
|
|
65
|
+
import { spec } from "../../setup/cli.specification.js";
|
|
66
|
+
|
|
67
|
+
test("builds the project", async () => {
|
|
68
|
+
// Given — sample app project
|
|
69
|
+
const result = await spec("build").project("sample-app").exec("build").run();
|
|
70
|
+
|
|
71
|
+
// Then — ESM output with source maps
|
|
72
|
+
result
|
|
73
|
+
.expectExitCode(0)
|
|
74
|
+
.expectStdoutContains("Build completed")
|
|
75
|
+
.expectFile("dist/index.js")
|
|
76
|
+
.expectNoFile("dist/index.cjs")
|
|
77
|
+
.expectFileContains("dist/index.js", "Hello");
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Specification runners
|
|
82
|
+
|
|
83
|
+
Three modes, same builder API. Each handles infrastructure and cleanup automatically.
|
|
84
|
+
|
|
85
|
+
### `integration()` — testcontainers + in-process app
|
|
86
|
+
|
|
87
|
+
Starts real containers via testcontainers. App runs in-process (Hono). Fastest feedback loop.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { integration, postgres, redis } from "@jterrazz/test";
|
|
91
|
+
|
|
92
|
+
const db = postgres({ compose: "db" });
|
|
93
|
+
const cache = redis({ compose: "cache" });
|
|
94
|
+
|
|
95
|
+
export const spec = await integration({
|
|
96
|
+
services: [db, cache],
|
|
97
|
+
app: () => createApp({ databaseUrl: db.connectionString }),
|
|
98
|
+
root: "../../",
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `e2e()` — docker compose up + real HTTP
|
|
103
|
+
|
|
104
|
+
Starts the full `docker/compose.test.yaml` stack. App URL and databases auto-detected.
|
|
37
105
|
|
|
38
106
|
```typescript
|
|
39
|
-
// tests/e2e/e2e.specification.ts
|
|
40
|
-
import { afterAll } from "vitest";
|
|
41
107
|
import { e2e } from "@jterrazz/test";
|
|
42
108
|
|
|
43
109
|
export const spec = await e2e({
|
|
44
110
|
root: "../../",
|
|
45
111
|
});
|
|
112
|
+
```
|
|
46
113
|
|
|
47
|
-
|
|
114
|
+
### `cli()` — local command execution
|
|
115
|
+
|
|
116
|
+
Runs CLI commands against fixture projects in temp directories. Optionally starts infrastructure.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { cli } from "@jterrazz/test";
|
|
120
|
+
|
|
121
|
+
export const spec = await cli({
|
|
122
|
+
command: resolve(import.meta.dirname, "../../bin/my-cli.sh"),
|
|
123
|
+
root: "../fixtures",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// With infrastructure (CLI that needs a database)
|
|
127
|
+
export const spec = await cli({
|
|
128
|
+
command: "my-migrate-tool",
|
|
129
|
+
root: "../fixtures",
|
|
130
|
+
services: [db],
|
|
131
|
+
});
|
|
48
132
|
```
|
|
49
133
|
|
|
50
|
-
|
|
134
|
+
## Builder API
|
|
135
|
+
|
|
136
|
+
Every test follows the same pattern: `spec("label") → setup → action → assertions`.
|
|
137
|
+
|
|
138
|
+
### Setup (cross-mode)
|
|
139
|
+
|
|
140
|
+
| Method | Description |
|
|
141
|
+
| ---------------------------------------- | -------------------------------------------------------- |
|
|
142
|
+
| `.seed("file.sql")` | Load SQL from `seeds/file.sql` into the default database |
|
|
143
|
+
| `.seed("file.sql", { service: "name" })` | Load SQL into a specific database |
|
|
144
|
+
| `.fixture("file")` | Copy `fixtures/file` into the CLI working directory |
|
|
145
|
+
| `.project("name")` | Use `fixtures/name/` as the CLI working directory |
|
|
146
|
+
| `.mock("file.json")` | Register mocked external API response (MSW, planned) |
|
|
147
|
+
|
|
148
|
+
### Actions (one per spec, mutually exclusive)
|
|
149
|
+
|
|
150
|
+
**HTTP:**
|
|
151
|
+
|
|
152
|
+
| Method | Description |
|
|
153
|
+
| -------------------------- | --------------------------------------------- |
|
|
154
|
+
| `.get(path)` | HTTP GET request |
|
|
155
|
+
| `.post(path, "body.json")` | HTTP POST with body from `requests/body.json` |
|
|
156
|
+
| `.put(path, "body.json")` | HTTP PUT with body from `requests/body.json` |
|
|
157
|
+
| `.delete(path)` | HTTP DELETE request |
|
|
158
|
+
|
|
159
|
+
**CLI:**
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
| -------------------------------------- | ----------------------------------------------------------- |
|
|
163
|
+
| `.exec("args")` | Run command (blocking) |
|
|
164
|
+
| `.exec(["build", "start"])` | Run commands sequentially in same directory |
|
|
165
|
+
| `.spawn("args", { waitFor, timeout })` | Run long-lived process, resolve on pattern match or timeout |
|
|
166
|
+
|
|
167
|
+
### Assertions
|
|
168
|
+
|
|
169
|
+
All assertions return `this` for chaining. Database assertions (`expectTable`) are async.
|
|
170
|
+
|
|
171
|
+
**HTTP-specific:**
|
|
172
|
+
|
|
173
|
+
| Method | Description |
|
|
174
|
+
| ------------------------------ | -------------------------------------------------- |
|
|
175
|
+
| `.expectStatus(code)` | Assert HTTP status code |
|
|
176
|
+
| `.expectResponse("file.json")` | Assert response body matches `responses/file.json` |
|
|
177
|
+
|
|
178
|
+
**CLI-specific:**
|
|
179
|
+
|
|
180
|
+
| Method | Description |
|
|
181
|
+
| ---------------------------- | ----------------------------------------- |
|
|
182
|
+
| `.expectExitCode(code)` | Assert process exit code |
|
|
183
|
+
| `.expectStdoutContains(str)` | Assert stdout contains string |
|
|
184
|
+
| `.expectStderrContains(str)` | Assert stderr contains string |
|
|
185
|
+
| `.expectStdout("file.txt")` | Assert stdout matches `expected/file.txt` |
|
|
186
|
+
| `.expectStderr("file.txt")` | Assert stderr matches `expected/file.txt` |
|
|
187
|
+
|
|
188
|
+
**Cross-mode:**
|
|
189
|
+
|
|
190
|
+
| Method | Description |
|
|
191
|
+
| ------------------------------------------------- | --------------------------------------- |
|
|
192
|
+
| `.expectTable(table, { columns, rows })` | Assert database table contents |
|
|
193
|
+
| `.expectTable(table, { columns, rows, service })` | Assert on a specific database |
|
|
194
|
+
| `.expectFile(path)` | Assert file exists in working directory |
|
|
195
|
+
| `.expectNoFile(path)` | Assert file does not exist |
|
|
196
|
+
| `.expectFileContains(path, content)` | Assert file contains string |
|
|
197
|
+
|
|
198
|
+
## Multi-database support
|
|
199
|
+
|
|
200
|
+
When multiple databases are declared, `seed()` and `expectTable()` accept `{ service: "name" }` to target a specific database by its compose name. Without `service`, both default to the first postgres.
|
|
51
201
|
|
|
52
202
|
```typescript
|
|
53
|
-
|
|
203
|
+
const db = postgres({ compose: "db" });
|
|
204
|
+
const analyticsDb = postgres({ compose: "analytics-db" });
|
|
54
205
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.run();
|
|
206
|
+
const spec = await integration({
|
|
207
|
+
services: [db, analyticsDb],
|
|
208
|
+
app: () => createApp({ ... }),
|
|
209
|
+
});
|
|
60
210
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
211
|
+
const result = await spec("cross-db")
|
|
212
|
+
.seed("users.sql")
|
|
213
|
+
.seed("events.sql", { service: "analytics-db" })
|
|
214
|
+
.post("/users", "request.json")
|
|
215
|
+
.run();
|
|
216
|
+
|
|
217
|
+
await result.expectTable("users", { columns: ["name"], rows: [["Alice"]] });
|
|
218
|
+
await result.expectTable("events", {
|
|
219
|
+
columns: ["type"],
|
|
220
|
+
rows: [["user_created"]],
|
|
221
|
+
service: "analytics-db",
|
|
67
222
|
});
|
|
68
223
|
```
|
|
69
224
|
|
|
@@ -72,60 +227,59 @@ test("creates company", async () => {
|
|
|
72
227
|
```typescript
|
|
73
228
|
import { postgres, redis } from "@jterrazz/test";
|
|
74
229
|
|
|
75
|
-
const db = postgres({ compose: "db" });
|
|
230
|
+
const db = postgres({ compose: "db" });
|
|
76
231
|
const cache = redis({ compose: "cache" });
|
|
77
232
|
```
|
|
78
233
|
|
|
79
|
-
|
|
234
|
+
Service handles read image and environment from `docker/compose.test.yaml`. After the runner starts, `.connectionString` is populated from the running container.
|
|
80
235
|
|
|
81
|
-
|
|
236
|
+
| Factory | Options | Connection string |
|
|
237
|
+
| ------------ | ------------------------- | ------------------------------------- |
|
|
238
|
+
| `postgres()` | `compose`, `image`, `env` | `postgresql://user:pass@host:port/db` |
|
|
239
|
+
| `redis()` | `compose`, `image` | `redis://host:port` |
|
|
82
240
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
│ └── init.sql # Auto-run on container start
|
|
241
|
+
## Mocking utilities
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { mockOf, mockOfDate } from "@jterrazz/test";
|
|
88
245
|
```
|
|
89
246
|
|
|
90
|
-
|
|
247
|
+
| Export | Description |
|
|
248
|
+
| ------------- | -------------------------------------------- |
|
|
249
|
+
| `mockOf<T>()` | Deep mock of any interface |
|
|
250
|
+
| `mockOfDate` | Date mocking via `.set(date)` and `.reset()` |
|
|
91
251
|
|
|
92
|
-
|
|
252
|
+
## Conventions
|
|
93
253
|
|
|
94
|
-
|
|
254
|
+
### Docker
|
|
95
255
|
|
|
96
|
-
|
|
256
|
+
```
|
|
257
|
+
docker/
|
|
258
|
+
├── compose.test.yaml # Source of truth for test infrastructure
|
|
259
|
+
├── postgres/
|
|
260
|
+
│ └── init.sql # Auto-run on container start
|
|
261
|
+
```
|
|
97
262
|
|
|
98
|
-
|
|
263
|
+
### Test structure
|
|
99
264
|
|
|
100
265
|
```
|
|
101
266
|
tests/
|
|
102
|
-
├──
|
|
103
|
-
|
|
104
|
-
├──
|
|
105
|
-
├──
|
|
106
|
-
│
|
|
107
|
-
│
|
|
108
|
-
│
|
|
109
|
-
│
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
## Test data (colocated per test)
|
|
119
|
-
|
|
120
|
-
| Folder | Purpose |
|
|
121
|
-
| ------------ | ---------------------------------- |
|
|
122
|
-
| `seeds/` | Database state setup |
|
|
123
|
-
| `mock/` | Mocked external API responses |
|
|
124
|
-
| `requests/` | Request bodies |
|
|
125
|
-
| `responses/` | Expected API responses |
|
|
126
|
-
| `expected/` | Expected output to compare against |
|
|
127
|
-
|
|
128
|
-
## File naming
|
|
267
|
+
├── e2e/ # Full-stack specification tests
|
|
268
|
+
│ └── {feature}/
|
|
269
|
+
│ ├── {feature}.e2e.test.ts
|
|
270
|
+
│ ├── seeds/ # Database state setup
|
|
271
|
+
│ ├── fixtures/ # Files copied into CLI working dir
|
|
272
|
+
│ ├── requests/ # HTTP request bodies
|
|
273
|
+
│ ├── responses/ # Expected HTTP responses
|
|
274
|
+
│ └── expected/ # Expected CLI output
|
|
275
|
+
├── integration/ # Infrastructure tests (containers)
|
|
276
|
+
└── setup/ # Specification runners, fixtures, helpers
|
|
277
|
+
├── fixtures/ # Shared fixture projects
|
|
278
|
+
├── helpers/ # Shared test utilities
|
|
279
|
+
└── *.specification.ts # Runner setup files
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### File naming
|
|
129
283
|
|
|
130
284
|
| Type | Suffix | Location |
|
|
131
285
|
| ----------- | ---------------------- | --------------------- |
|
|
@@ -133,13 +287,31 @@ tests/
|
|
|
133
287
|
| Integration | `.integration.test.ts` | `tests/integration/` |
|
|
134
288
|
| E2E | `.e2e.test.ts` | `tests/e2e/` |
|
|
135
289
|
|
|
136
|
-
|
|
290
|
+
### Test writing
|
|
291
|
+
|
|
292
|
+
Every test uses `// Given` and `// Then` comments. Always both, never one without the other.
|
|
137
293
|
|
|
138
294
|
```typescript
|
|
139
|
-
|
|
295
|
+
test("creates a user and returns 201", async () => {
|
|
296
|
+
// Given — two existing users
|
|
297
|
+
const result = await spec("creates user")
|
|
298
|
+
.seed("initial-users.sql")
|
|
299
|
+
.post("/users", "new-user.json")
|
|
300
|
+
.run();
|
|
301
|
+
|
|
302
|
+
// Then — user created with all three in table
|
|
303
|
+
result.expectStatus(201);
|
|
304
|
+
await result.expectTable("users", {
|
|
305
|
+
columns: ["name"],
|
|
306
|
+
rows: [["Alice"], ["Bob"], ["Charlie"]],
|
|
307
|
+
});
|
|
308
|
+
});
|
|
140
309
|
```
|
|
141
310
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
311
|
+
`// When` is only used if the action isn't obvious. The spec builder chain (`.seed().post().run()` / `.project().exec().run()`) IS the when.
|
|
312
|
+
|
|
313
|
+
## Requirements
|
|
314
|
+
|
|
315
|
+
- **Docker** — testcontainers for `integration()`, docker compose for `e2e()`
|
|
316
|
+
- **vitest** — peer dependency
|
|
317
|
+
- **hono** — optional peer, only needed for `integration()` mode with in-process apps
|