@grest-ts/testkit 0.0.6 → 0.0.8

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.
Files changed (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +413 -413
  3. package/dist/src/runner/isolated-loader.mjs +91 -91
  4. package/dist/src/runner/worker-loader.mjs +49 -49
  5. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  6. package/package.json +13 -13
  7. package/src/GGBundleTest.ts +89 -89
  8. package/src/GGTest.ts +318 -318
  9. package/src/GGTestContext.ts +74 -74
  10. package/src/GGTestRunner.ts +308 -308
  11. package/src/GGTestRuntime.ts +265 -265
  12. package/src/GGTestRuntimeWorker.ts +159 -159
  13. package/src/GGTestSharedRef.ts +116 -116
  14. package/src/GGTestkitExtensionsDiscovery.ts +26 -26
  15. package/src/IGGLocalDiscoveryServer.ts +16 -16
  16. package/src/callOn/GGCallOnSelector.ts +61 -61
  17. package/src/callOn/GGContractClass.implement.ts +43 -43
  18. package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -134
  19. package/src/callOn/TestableIPC.ts +81 -81
  20. package/src/callOn/callOn.ts +224 -224
  21. package/src/callOn/registerOnCallHandler.ts +123 -123
  22. package/src/index-node.ts +64 -64
  23. package/src/mockable/GGMockable.ts +22 -22
  24. package/src/mockable/GGMockableCall.ts +45 -45
  25. package/src/mockable/GGMockableIPC.ts +20 -20
  26. package/src/mockable/GGMockableInterceptor.ts +44 -44
  27. package/src/mockable/GGMockableInterceptorsServer.ts +69 -69
  28. package/src/mockable/mockable.ts +71 -71
  29. package/src/runner/InlineRunner.ts +47 -47
  30. package/src/runner/IsolatedRunner.ts +179 -179
  31. package/src/runner/RuntimeRunner.ts +15 -15
  32. package/src/runner/WorkerRunner.ts +179 -179
  33. package/src/runner/isolated-loader.mjs +91 -91
  34. package/src/runner/worker-loader.mjs +49 -49
  35. package/src/testers/GGCallInterceptor.ts +224 -224
  36. package/src/testers/GGMockWith.ts +92 -92
  37. package/src/testers/GGSpyWith.ts +115 -115
  38. package/src/testers/GGTestAction.ts +332 -332
  39. package/src/testers/GGTestComponent.ts +16 -16
  40. package/src/testers/GGTestSelector.ts +223 -223
  41. package/src/testers/IGGTestInterceptor.ts +10 -10
  42. package/src/testers/IGGTestWith.ts +15 -15
  43. package/src/testers/RuntimeSelector.ts +151 -151
  44. package/src/utils/GGExpectations.ts +78 -78
  45. package/src/utils/GGTestError.ts +36 -36
  46. package/src/utils/captureStack.ts +53 -53
package/README.md CHANGED
@@ -3,416 +3,416 @@
3
3
  > [Documentation](https://github.com/grest-ts/grest-ts#readme) | [All packages](https://github.com/grest-ts/grest-ts#package-reference)
4
4
  <!-- GREST-TS-BANNER-END -->
5
5
 
6
- # Testkit
7
-
8
- ## Overview
9
-
10
- The testkit provides comprehensive testing capabilities to the framework, mostly focusing on backend integration & component level testing.
11
-
12
- ### Main features:
13
-
14
- * Runs on top of vitest - all normal vitest features are available!
15
- * Component & integration testing
16
- * Parallel execution of tests.
17
- * Test a single or multiple components.
18
- * Mock or spy communication between runtimes.
19
- * Mock external dependencies (like third party API-s) using @mockable decorator.
20
- * Manage databases (clone, seed, etc...).
21
- * Unit testing
22
- * Standard vitest and its capabilities. Nothing too special here.
23
-
24
- Overall idea is that you get to run your services as if they were running in production, but get access to where you need - like:
25
-
26
- * accessing logs
27
- * accessing metrics
28
- * accessing & updating config
29
- * mock or spy on API calls / external requests / events etc...
30
- * easy setup for isolated databases.
31
-
32
- To fully understand the fun, you have to try it out on how easy and fast testing microservice environments can become.
33
- Start from a single service and easily scale up to multi service environments with ease.
34
- This is a capability easy to underestimate when starting a project, but you absolutely love it when things start scaling up!
35
-
36
- ## Capabilities example
37
-
38
- ```typescript
39
- import {GGTest} from "@grest-ts/testkit";
40
- import {MyRuntime} from "./MyRuntime";
41
- import {MyApi} from "./MyApi.api";
42
-
43
- describe("my test", () => {
44
- const t = GGTest.startInline(MyRuntime);
45
- const client = MyApi.createTestClient();
46
-
47
- test("selector extensions", async () => {
48
- // t.myRuntime.logs - from @grest-ts/logger/testkit
49
- const cursor = await t.myRuntime.logs.cursor();
50
- await client.doSomething();
51
- const logs = await cursor.retrieve();
52
-
53
- // t.myRuntime.config - from @grest-ts/config/testkit
54
- await t.myRuntime.config.update(MY_CONFIG_KEY, newValue);
55
-
56
- // t.all() selects all runtimes
57
- await t.all().config.update(SHARED_KEY, value);
58
- });
59
-
60
- test("schema extensions - mock/spy", async () => {
61
- // MyApi.mock.methodName - from @grest-ts/http/testkit
62
- await client.getUser({id: 1})
63
- .with(MyApi.mock.getUser.andReturn({name: "Alice"}));
64
-
65
- // MyApi.spy.methodName - passthrough with validation
66
- await client.createUser({name: "Bob"})
67
- .with(MyApi.spy.createUser.toMatchObject({name: "Bob"}));
68
- });
69
-
70
- test("log expectations with .with()", async () => {
71
- await client.doSomething()
72
- .with(t.myRuntime.logs.expect("expected message"));
73
- });
74
-
75
- test("waitFor - wait for async side effects", async () => {
76
- await client.triggerAsyncProcess()
77
- .waitFor(t.myRuntime.logs.expect("process completed"));
78
- });
79
- });
80
- ```
81
-
82
- ---
83
-
84
- ## GGTestContext
85
-
86
- `GGTestContext` is the recommended way to make API calls in tests. It extends `GGContext` and provides lifecycle hooks, API registration, and context management (e.g. auth headers).
87
-
88
- ### Basic usage
89
-
90
- ```typescript
91
- import {GGTest, GGTestContext} from "@grest-ts/testkit";
92
- import {MyRuntime} from "../src/main";
93
- import {UserApi} from "../src/api/UserApi.api";
94
- import {ChecklistApi} from "../src/api/ChecklistApi.api";
95
-
96
- describe("my tests", () => {
97
- GGTest.startWorker(MyRuntime);
98
-
99
- const alice = new GGTestContext("Alice")
100
- .apis({
101
- user: UserApi,
102
- checklist: ChecklistApi
103
- })
104
- .beforeAll(async () => {
105
- const result = await alice.user.register({
106
- username: "alice", email: "alice@example.com", password: "secret123"
107
- });
108
- alice.set(AUTH_TOKEN, result.token);
109
- });
110
-
111
- test('list items', async () => {
112
- await alice.checklist.list().toMatchObject([]);
113
- });
114
- });
115
- ```
116
-
117
- ### `.apis()`
118
-
119
- Registers APIs and returns `this` merged with test clients for all registered items.
120
- After calling `.apis()`, you can access each API directly as a property.
121
-
122
- ```typescript
123
- const admin = new GGTestContext("Admin")
124
- .apis({
125
- building: BuildingApi,
126
- apartment: ApartmentApi,
127
- client: ClientApi
128
- });
129
-
130
- // Now call API methods directly:
131
- await admin.building.sync({...});
132
- await admin.apartment.list({...});
133
- ```
134
-
135
- ### `.callOn()`
136
-
137
- Call methods on APIs or services not registered via `.apis()`.
138
-
139
- ```typescript
140
- // Call an API you didn't register upfront
141
- await alice.callOn(UserPublicApi).login({username: "alice", password: "secret123"});
142
- ```
143
-
144
- ### Lifecycle hooks
145
-
146
- ```typescript
147
- const alice = new GGTestContext("Alice")
148
- .apis({user: UserApi})
149
- .resetAfterEach() // Reset context state (headers etc) after each test
150
- .beforeAll(async () => { /* once */ }) // Runs once before all tests
151
- .beforeEach(async () => { /* each */ }) // Runs before each test
152
- .afterEach(async () => { /* each */ }) // Runs after each test
153
- .afterAll(async () => { /* cleanup */ }); // Runs once after all tests
154
- ```
155
-
156
- ### Context state management
157
-
158
- `GGTestContext` extends `GGContext`, so you can set/get/delete context values (typically auth headers):
159
-
160
- ```typescript
161
- alice.set(AUTH_TOKEN, token); // Set a context value (sent as header)
162
- alice.get(AUTH_TOKEN); // Read a context value
163
- alice.delete(AUTH_TOKEN); // Remove a context value
164
- ```
165
-
166
- ### Extending GGTestContext
167
-
168
- For domain-specific helpers, extend `GGTestContext`:
169
-
170
- ```typescript
171
- import {GGTestContext} from "@grest-ts/testkit";
172
-
173
- export class MyUserContext extends GGTestContext {
174
-
175
- public user: User;
176
-
177
- public async login(data: {username: string, password: string}) {
178
- const result = await this.callOn(UserPublicApi).login(data);
179
- this.set(AUTH_TOKEN, result.token);
180
- this.user = result.user;
181
- }
182
-
183
- public async loginAndSetup() {
184
- await this.login({username: "admin", password: "admin"});
185
- }
186
- }
187
-
188
- // Usage in tests:
189
- const admin = new MyUserContext("Admin")
190
- .apis({checklist: ChecklistApi})
191
- .beforeAll(async () => {
192
- await admin.loginAndSetup();
193
- });
194
- ```
195
-
196
- ---
197
-
198
- ## Response assertions
199
-
200
- All API calls through `GGTestContext` return a `GGTestAction` - a PromiseLike with chainable assertion methods. Assertions are checked when the action is awaited.
201
-
202
- ### Data assertions
203
-
204
- ```typescript
205
- // Exact match
206
- await alice.user.get({id}).toEqual({id, username: "alice", email: "alice@example.com"});
207
-
208
- // Partial match (like Jest toMatchObject)
209
- await alice.user.get({id}).toMatchObject({username: "alice"});
210
-
211
- // Undefined (for void endpoints)
212
- await alice.user.delete({id}).toBeUndefined();
213
-
214
- // Array length
215
- await alice.checklist.list().toHaveLength(3);
216
-
217
- // Array containment
218
- await alice.checklist.list()
219
- .arrayToContain({title: "Buy groceries"});
220
- ```
221
-
222
- ### Error assertions
223
-
224
- When testing error responses, use `.toBeError()` with an error class. After `.toBeError()`, further `.toMatchObject()` calls check the error data.
225
-
226
- ```typescript
227
- import {NOT_AUTHORIZED, VALIDATION_ERROR, NOT_FOUND, FORBIDDEN, EXISTS} from "@grest-ts/schema";
228
-
229
- // Authorization errors
230
- await john.building.sync({...}).toBeError(NOT_AUTHORIZED);
231
-
232
- // Validation errors - check individual field errors
233
- await alice.user.register({username: "ab", password: "", email: "invalid"})
234
- .toBeError(VALIDATION_ERROR)
235
- .toMatchObject({
236
- username: {__issue: {message: "Value must be between 3 and 10 characters"}},
237
- email: {__issue: {message: "Invalid email format"}},
238
- });
239
-
240
- // Not found
241
- await alice.user.get({id: 999}).toBeError(NOT_FOUND);
242
-
243
- // Custom error types
244
- await alice.user.login({username: "alice", password: "wrong"})
245
- .toBeError(InvalidCredentialsError);
246
- ```
247
-
248
- ### Chaining with interceptors
249
-
250
- Assertions chain naturally with `.with()` and `.waitFor()`:
251
-
252
- ```typescript
253
- await alice.user.login(loginData)
254
- .with(BlockerApi.spy.checkBlock.toMatchObject({username: "alice"}))
255
- .toMatchObject({user: {username: "alice"}, token: expect.any(String)});
256
- ```
257
-
258
- ---
259
-
260
- ## Database cloning
261
-
262
- The testkit provides database cloning for test isolation. Each test suite gets its own database clone, ensuring parallel test execution without conflicts.
263
-
264
- ### MySQL
265
-
266
- ```typescript
267
- import {GGTest} from "@grest-ts/testkit";
268
- import {MyConfig} from "../src/MyConfig";
269
- import {mysqlLocal} from "../config/local";
270
-
271
- describe("my tests", () => {
272
- GGTest.startWorker(MyRuntime);
273
-
274
- // Basic clone with explicit source config
275
- GGTest.with(MyConfig.mysql).clone({from: mysqlLocal});
276
-
277
- // With seed files
278
- GGTest.with(MyConfig.mysql).clone({
279
- from: mysqlLocal,
280
- seedFiles: ["./test/seed/admin-users.sql"]
281
- });
282
-
283
- // Shared clone across parallel workers (same DB for all workers in this group)
284
- GGTest.with(MyConfig.mysql).clone({
285
- from: mysqlLocal,
286
- group: "shared",
287
- seedFiles: ["./test/seed/admin-users.sql"]
288
- });
289
- });
290
- ```
291
-
292
- ### PostgreSQL
293
-
294
- ```typescript
295
- // Same API, just with postgres config
296
- GGTest.with(MyConfig.postgres).clone({from: postgresLocal});
297
- GGTest.with(MyConfig.postgres).clone({from: postgresLocal, group: "shared"});
298
- ```
299
-
300
- ### Shorthand forms
301
-
302
- ```typescript
303
- // If GGResource has a default value, no `from` needed:
304
- GGTest.with(MyConfig.mysql).clone();
305
-
306
- // Seed files as string or array:
307
- GGTest.with(MyConfig.mysql).clone("seed.sql");
308
- GGTest.with(MyConfig.mysql).clone(["seed1.sql", "seed2.sql"]);
309
- ```
310
-
311
- ### Clone options
312
-
313
- | Option | Type | Description |
314
- |--------|------|-------------|
315
- | `from` | `{host, user}` | Source database config and credentials. Required when GGResource has no default value. |
316
- | `seedFiles` | `string[]` | SQL files to run after cloning the schema. |
317
- | `group` | `string` | Group name for shared DB cloning across workers. Tests with the same group share one clone. |
318
-
319
- ### How it works
320
-
321
- 1. The source database schema is cloned into a unique test schema (named `{db}_{runId}_{groupId}`).
322
- 2. If the source database doesn't exist and a `schemaFile` is configured, it is created automatically.
323
- 3. Seed files are executed after cloning.
324
- 4. The cloned schema is automatically cleaned up after tests complete.
325
- 5. When `group` is specified, multiple test suites share the same clone via reference counting.
326
-
327
- ---
328
-
329
- ## Mocking & spying on @mockable services
330
-
331
- Services decorated with `@mockable` can be mocked or spied on in tests using `mockOf()` and `spyOn()`.
332
-
333
- This is different from schema-level mock/spy (like `MyApi.mock.method`) which intercepts HTTP calls between runtimes.
334
- `mockOf`/`spyOn` intercept calls to internal services within a runtime.
335
-
336
- ### mockOf() - replace with fake data
337
-
338
- ```typescript
339
- import {GGTest, mockOf} from "@grest-ts/testkit";
340
-
341
- test('mock external service', async () => {
342
- await alice.checklist.add({title: "Visit Times Square", address: "123 Main St"})
343
- .with(mockOf(AddressResolverService).resolveAddress
344
- .toEqual({address: "123 Main St"}) // Validate input
345
- .andReturn({lat: 40.7589, lng: -73.9851}) // Return fake data
346
- )
347
- .toMatchObject({
348
- title: "Visit Times Square",
349
- lat: 40.7589,
350
- lng: -73.9851
351
- });
352
- });
353
- ```
354
-
355
- ### spyOn() - call through and validate
356
-
357
- ```typescript
358
- import {GGTest, spyOn} from "@grest-ts/testkit";
359
-
360
- test('spy on external service', async () => {
361
- await alice.checklist.add({title: "Visit Times Square", address: "123 Main St"})
362
- .with(spyOn(AddressResolverService).resolveAddress
363
- .toEqual({address: "123 Main St"}) // Validate input
364
- .responseToMatchObject({lat: 40.7128, lng: -74.0060}) // Validate real response
365
- );
366
- });
367
- ```
368
-
369
- ### Mock/spy options
370
-
371
- ```typescript
372
- // Mock with expected call count
373
- .with(mockOf(Service).method
374
- .andReturn(result)
375
- .times(2)) // Expect exactly 2 calls
376
-
377
- // Mock with delay
378
- .with(mockOf(Service).method
379
- .andReturn(result)
380
- .sleep(100)) // Wait 100ms before returning
381
-
382
- // Mock returning an error
383
- .with(mockOf(Service).method
384
- .andReturn(new NOT_AUTHORIZED()))
385
-
386
- // Spy switching to response validation
387
- .with(spyOn(Service).method
388
- .toMatchObject({input: "data"}) // Validate input
389
- .response.toMatchObject({out: 1}) // Switch to response, then validate
390
- )
391
-
392
- // Spy expecting error response
393
- .with(spyOn(Service).method
394
- .toBeError(NOT_FOUND)
395
- )
396
- ```
397
-
398
- ### Schema mock/spy vs @mockable mock/spy
399
-
400
- | | Schema mock/spy (`MyApi.mock.method`) | @mockable mock/spy (`mockOf(Service).method`) |
401
- |---|---|---|
402
- | **What it intercepts** | HTTP calls between runtimes | Internal method calls within a runtime |
403
- | **Use case** | Mock/spy on service-to-service communication | Mock/spy on external dependencies (3rd party APIs, etc.) |
404
- | **How to use** | `MyApi.mock.method.andReturn(...)` | `mockOf(Service).method.andReturn(...)` |
405
- | **Import** | `import "@grest-ts/http/testkit"` | `import {mockOf, spyOn} from "@grest-ts/testkit"` |
406
-
407
- ---
408
-
409
- ## Usage
410
-
411
- Check README-testkit.md files within packages you are interested in. Common ones being:
412
-
413
- - [Logger](../../packages/logger/logger/README-testkit.md) - Accessing logs during the test flow.
414
- - [Metrics](../../packages/metrics/README-testkit.md) - Accessing metrics during the test flow.
415
-
416
- ## Extending framework with custom packages and adding testkit capabilities.
417
-
418
- - [Extending Guide](./README-extending.md) - How to create testkit extensions - adding capabilities to the testing framework for custom packages.
6
+ # Testkit
7
+
8
+ ## Overview
9
+
10
+ The testkit provides comprehensive testing capabilities to the framework, mostly focusing on backend integration & component level testing.
11
+
12
+ ### Main features:
13
+
14
+ * Runs on top of vitest - all normal vitest features are available!
15
+ * Component & integration testing
16
+ * Parallel execution of tests.
17
+ * Test a single or multiple components.
18
+ * Mock or spy communication between runtimes.
19
+ * Mock external dependencies (like third party API-s) using @mockable decorator.
20
+ * Manage databases (clone, seed, etc...).
21
+ * Unit testing
22
+ * Standard vitest and its capabilities. Nothing too special here.
23
+
24
+ Overall idea is that you get to run your services as if they were running in production, but get access to where you need - like:
25
+
26
+ * accessing logs
27
+ * accessing metrics
28
+ * accessing & updating config
29
+ * mock or spy on API calls / external requests / events etc...
30
+ * easy setup for isolated databases.
31
+
32
+ To fully understand the fun, you have to try it out on how easy and fast testing microservice environments can become.
33
+ Start from a single service and easily scale up to multi service environments with ease.
34
+ This is a capability easy to underestimate when starting a project, but you absolutely love it when things start scaling up!
35
+
36
+ ## Capabilities example
37
+
38
+ ```typescript
39
+ import {GGTest} from "@grest-ts/testkit";
40
+ import {MyRuntime} from "./MyRuntime";
41
+ import {MyApi} from "./MyApi.api";
42
+
43
+ describe("my test", () => {
44
+ const t = GGTest.startInline(MyRuntime);
45
+ const client = MyApi.createTestClient();
46
+
47
+ test("selector extensions", async () => {
48
+ // t.myRuntime.logs - from @grest-ts/logger/testkit
49
+ const cursor = await t.myRuntime.logs.cursor();
50
+ await client.doSomething();
51
+ const logs = await cursor.retrieve();
52
+
53
+ // t.myRuntime.config - from @grest-ts/config/testkit
54
+ await t.myRuntime.config.update(MY_CONFIG_KEY, newValue);
55
+
56
+ // t.all() selects all runtimes
57
+ await t.all().config.update(SHARED_KEY, value);
58
+ });
59
+
60
+ test("schema extensions - mock/spy", async () => {
61
+ // MyApi.mock.methodName - from @grest-ts/http/testkit
62
+ await client.getUser({id: 1})
63
+ .with(MyApi.mock.getUser.andReturn({name: "Alice"}));
64
+
65
+ // MyApi.spy.methodName - passthrough with validation
66
+ await client.createUser({name: "Bob"})
67
+ .with(MyApi.spy.createUser.toMatchObject({name: "Bob"}));
68
+ });
69
+
70
+ test("log expectations with .with()", async () => {
71
+ await client.doSomething()
72
+ .with(t.myRuntime.logs.expect("expected message"));
73
+ });
74
+
75
+ test("waitFor - wait for async side effects", async () => {
76
+ await client.triggerAsyncProcess()
77
+ .waitFor(t.myRuntime.logs.expect("process completed"));
78
+ });
79
+ });
80
+ ```
81
+
82
+ ---
83
+
84
+ ## GGTestContext
85
+
86
+ `GGTestContext` is the recommended way to make API calls in tests. It extends `GGContext` and provides lifecycle hooks, API registration, and context management (e.g. auth headers).
87
+
88
+ ### Basic usage
89
+
90
+ ```typescript
91
+ import {GGTest, GGTestContext} from "@grest-ts/testkit";
92
+ import {MyRuntime} from "../src/main";
93
+ import {UserApi} from "../src/api/UserApi.api";
94
+ import {ChecklistApi} from "../src/api/ChecklistApi.api";
95
+
96
+ describe("my tests", () => {
97
+ GGTest.startWorker(MyRuntime);
98
+
99
+ const alice = new GGTestContext("Alice")
100
+ .apis({
101
+ user: UserApi,
102
+ checklist: ChecklistApi
103
+ })
104
+ .beforeAll(async () => {
105
+ const result = await alice.user.register({
106
+ username: "alice", email: "alice@example.com", password: "secret123"
107
+ });
108
+ alice.set(AUTH_TOKEN, result.token);
109
+ });
110
+
111
+ test('list items', async () => {
112
+ await alice.checklist.list().toMatchObject([]);
113
+ });
114
+ });
115
+ ```
116
+
117
+ ### `.apis()`
118
+
119
+ Registers APIs and returns `this` merged with test clients for all registered items.
120
+ After calling `.apis()`, you can access each API directly as a property.
121
+
122
+ ```typescript
123
+ const admin = new GGTestContext("Admin")
124
+ .apis({
125
+ building: BuildingApi,
126
+ apartment: ApartmentApi,
127
+ client: ClientApi
128
+ });
129
+
130
+ // Now call API methods directly:
131
+ await admin.building.sync({...});
132
+ await admin.apartment.list({...});
133
+ ```
134
+
135
+ ### `.callOn()`
136
+
137
+ Call methods on APIs or services not registered via `.apis()`.
138
+
139
+ ```typescript
140
+ // Call an API you didn't register upfront
141
+ await alice.callOn(UserPublicApi).login({username: "alice", password: "secret123"});
142
+ ```
143
+
144
+ ### Lifecycle hooks
145
+
146
+ ```typescript
147
+ const alice = new GGTestContext("Alice")
148
+ .apis({user: UserApi})
149
+ .resetAfterEach() // Reset context state (headers etc) after each test
150
+ .beforeAll(async () => { /* once */ }) // Runs once before all tests
151
+ .beforeEach(async () => { /* each */ }) // Runs before each test
152
+ .afterEach(async () => { /* each */ }) // Runs after each test
153
+ .afterAll(async () => { /* cleanup */ }); // Runs once after all tests
154
+ ```
155
+
156
+ ### Context state management
157
+
158
+ `GGTestContext` extends `GGContext`, so you can set/get/delete context values (typically auth headers):
159
+
160
+ ```typescript
161
+ alice.set(AUTH_TOKEN, token); // Set a context value (sent as header)
162
+ alice.get(AUTH_TOKEN); // Read a context value
163
+ alice.delete(AUTH_TOKEN); // Remove a context value
164
+ ```
165
+
166
+ ### Extending GGTestContext
167
+
168
+ For domain-specific helpers, extend `GGTestContext`:
169
+
170
+ ```typescript
171
+ import {GGTestContext} from "@grest-ts/testkit";
172
+
173
+ export class MyUserContext extends GGTestContext {
174
+
175
+ public user: User;
176
+
177
+ public async login(data: {username: string, password: string}) {
178
+ const result = await this.callOn(UserPublicApi).login(data);
179
+ this.set(AUTH_TOKEN, result.token);
180
+ this.user = result.user;
181
+ }
182
+
183
+ public async loginAndSetup() {
184
+ await this.login({username: "admin", password: "admin"});
185
+ }
186
+ }
187
+
188
+ // Usage in tests:
189
+ const admin = new MyUserContext("Admin")
190
+ .apis({checklist: ChecklistApi})
191
+ .beforeAll(async () => {
192
+ await admin.loginAndSetup();
193
+ });
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Response assertions
199
+
200
+ All API calls through `GGTestContext` return a `GGTestAction` - a PromiseLike with chainable assertion methods. Assertions are checked when the action is awaited.
201
+
202
+ ### Data assertions
203
+
204
+ ```typescript
205
+ // Exact match
206
+ await alice.user.get({id}).toEqual({id, username: "alice", email: "alice@example.com"});
207
+
208
+ // Partial match (like Jest toMatchObject)
209
+ await alice.user.get({id}).toMatchObject({username: "alice"});
210
+
211
+ // Undefined (for void endpoints)
212
+ await alice.user.delete({id}).toBeUndefined();
213
+
214
+ // Array length
215
+ await alice.checklist.list().toHaveLength(3);
216
+
217
+ // Array containment
218
+ await alice.checklist.list()
219
+ .arrayToContain({title: "Buy groceries"});
220
+ ```
221
+
222
+ ### Error assertions
223
+
224
+ When testing error responses, use `.toBeError()` with an error class. After `.toBeError()`, further `.toMatchObject()` calls check the error data.
225
+
226
+ ```typescript
227
+ import {NOT_AUTHORIZED, VALIDATION_ERROR, NOT_FOUND, FORBIDDEN, EXISTS} from "@grest-ts/schema";
228
+
229
+ // Authorization errors
230
+ await john.building.sync({...}).toBeError(NOT_AUTHORIZED);
231
+
232
+ // Validation errors - check individual field errors
233
+ await alice.user.register({username: "ab", password: "", email: "invalid"})
234
+ .toBeError(VALIDATION_ERROR)
235
+ .toMatchObject({
236
+ username: {__issue: {message: "Value must be between 3 and 10 characters"}},
237
+ email: {__issue: {message: "Invalid email format"}},
238
+ });
239
+
240
+ // Not found
241
+ await alice.user.get({id: 999}).toBeError(NOT_FOUND);
242
+
243
+ // Custom error types
244
+ await alice.user.login({username: "alice", password: "wrong"})
245
+ .toBeError(InvalidCredentialsError);
246
+ ```
247
+
248
+ ### Chaining with interceptors
249
+
250
+ Assertions chain naturally with `.with()` and `.waitFor()`:
251
+
252
+ ```typescript
253
+ await alice.user.login(loginData)
254
+ .with(BlockerApi.spy.checkBlock.toMatchObject({username: "alice"}))
255
+ .toMatchObject({user: {username: "alice"}, token: expect.any(String)});
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Database cloning
261
+
262
+ The testkit provides database cloning for test isolation. Each test suite gets its own database clone, ensuring parallel test execution without conflicts.
263
+
264
+ ### MySQL
265
+
266
+ ```typescript
267
+ import {GGTest} from "@grest-ts/testkit";
268
+ import {MyConfig} from "../src/MyConfig";
269
+ import {mysqlLocal} from "../config/local";
270
+
271
+ describe("my tests", () => {
272
+ GGTest.startWorker(MyRuntime);
273
+
274
+ // Basic clone with explicit source config
275
+ GGTest.with(MyConfig.mysql).clone({from: mysqlLocal});
276
+
277
+ // With seed files
278
+ GGTest.with(MyConfig.mysql).clone({
279
+ from: mysqlLocal,
280
+ seedFiles: ["./test/seed/admin-users.sql"]
281
+ });
282
+
283
+ // Shared clone across parallel workers (same DB for all workers in this group)
284
+ GGTest.with(MyConfig.mysql).clone({
285
+ from: mysqlLocal,
286
+ group: "shared",
287
+ seedFiles: ["./test/seed/admin-users.sql"]
288
+ });
289
+ });
290
+ ```
291
+
292
+ ### PostgreSQL
293
+
294
+ ```typescript
295
+ // Same API, just with postgres config
296
+ GGTest.with(MyConfig.postgres).clone({from: postgresLocal});
297
+ GGTest.with(MyConfig.postgres).clone({from: postgresLocal, group: "shared"});
298
+ ```
299
+
300
+ ### Shorthand forms
301
+
302
+ ```typescript
303
+ // If GGResource has a default value, no `from` needed:
304
+ GGTest.with(MyConfig.mysql).clone();
305
+
306
+ // Seed files as string or array:
307
+ GGTest.with(MyConfig.mysql).clone("seed.sql");
308
+ GGTest.with(MyConfig.mysql).clone(["seed1.sql", "seed2.sql"]);
309
+ ```
310
+
311
+ ### Clone options
312
+
313
+ | Option | Type | Description |
314
+ |--------|------|-------------|
315
+ | `from` | `{host, user}` | Source database config and credentials. Required when GGResource has no default value. |
316
+ | `seedFiles` | `string[]` | SQL files to run after cloning the schema. |
317
+ | `group` | `string` | Group name for shared DB cloning across workers. Tests with the same group share one clone. |
318
+
319
+ ### How it works
320
+
321
+ 1. The source database schema is cloned into a unique test schema (named `{db}_{runId}_{groupId}`).
322
+ 2. If the source database doesn't exist and a `schemaFile` is configured, it is created automatically.
323
+ 3. Seed files are executed after cloning.
324
+ 4. The cloned schema is automatically cleaned up after tests complete.
325
+ 5. When `group` is specified, multiple test suites share the same clone via reference counting.
326
+
327
+ ---
328
+
329
+ ## Mocking & spying on @mockable services
330
+
331
+ Services decorated with `@mockable` can be mocked or spied on in tests using `mockOf()` and `spyOn()`.
332
+
333
+ This is different from schema-level mock/spy (like `MyApi.mock.method`) which intercepts HTTP calls between runtimes.
334
+ `mockOf`/`spyOn` intercept calls to internal services within a runtime.
335
+
336
+ ### mockOf() - replace with fake data
337
+
338
+ ```typescript
339
+ import {GGTest, mockOf} from "@grest-ts/testkit";
340
+
341
+ test('mock external service', async () => {
342
+ await alice.checklist.add({title: "Visit Times Square", address: "123 Main St"})
343
+ .with(mockOf(AddressResolverService).resolveAddress
344
+ .toEqual({address: "123 Main St"}) // Validate input
345
+ .andReturn({lat: 40.7589, lng: -73.9851}) // Return fake data
346
+ )
347
+ .toMatchObject({
348
+ title: "Visit Times Square",
349
+ lat: 40.7589,
350
+ lng: -73.9851
351
+ });
352
+ });
353
+ ```
354
+
355
+ ### spyOn() - call through and validate
356
+
357
+ ```typescript
358
+ import {GGTest, spyOn} from "@grest-ts/testkit";
359
+
360
+ test('spy on external service', async () => {
361
+ await alice.checklist.add({title: "Visit Times Square", address: "123 Main St"})
362
+ .with(spyOn(AddressResolverService).resolveAddress
363
+ .toEqual({address: "123 Main St"}) // Validate input
364
+ .responseToMatchObject({lat: 40.7128, lng: -74.0060}) // Validate real response
365
+ );
366
+ });
367
+ ```
368
+
369
+ ### Mock/spy options
370
+
371
+ ```typescript
372
+ // Mock with expected call count
373
+ .with(mockOf(Service).method
374
+ .andReturn(result)
375
+ .times(2)) // Expect exactly 2 calls
376
+
377
+ // Mock with delay
378
+ .with(mockOf(Service).method
379
+ .andReturn(result)
380
+ .sleep(100)) // Wait 100ms before returning
381
+
382
+ // Mock returning an error
383
+ .with(mockOf(Service).method
384
+ .andReturn(new NOT_AUTHORIZED()))
385
+
386
+ // Spy switching to response validation
387
+ .with(spyOn(Service).method
388
+ .toMatchObject({input: "data"}) // Validate input
389
+ .response.toMatchObject({out: 1}) // Switch to response, then validate
390
+ )
391
+
392
+ // Spy expecting error response
393
+ .with(spyOn(Service).method
394
+ .toBeError(NOT_FOUND)
395
+ )
396
+ ```
397
+
398
+ ### Schema mock/spy vs @mockable mock/spy
399
+
400
+ | | Schema mock/spy (`MyApi.mock.method`) | @mockable mock/spy (`mockOf(Service).method`) |
401
+ |---|---|---|
402
+ | **What it intercepts** | HTTP calls between runtimes | Internal method calls within a runtime |
403
+ | **Use case** | Mock/spy on service-to-service communication | Mock/spy on external dependencies (3rd party APIs, etc.) |
404
+ | **How to use** | `MyApi.mock.method.andReturn(...)` | `mockOf(Service).method.andReturn(...)` |
405
+ | **Import** | `import "@grest-ts/http/testkit"` | `import {mockOf, spyOn} from "@grest-ts/testkit"` |
406
+
407
+ ---
408
+
409
+ ## Usage
410
+
411
+ Check README-testkit.md files within packages you are interested in. Common ones being:
412
+
413
+ - [Logger](../../packages/logger/logger/README-testkit.md) - Accessing logs during the test flow.
414
+ - [Metrics](../../packages/metrics/README-testkit.md) - Accessing metrics during the test flow.
415
+
416
+ ## Extending framework with custom packages and adding testkit capabilities.
417
+
418
+ - [Extending Guide](./README-extending.md) - How to create testkit extensions - adding capabilities to the testing framework for custom packages.