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