@ddd-ts-kit/testing 0.15.0 → 0.16.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.
Files changed (3) hide show
  1. package/README.fr.md +969 -0
  2. package/README.md +356 -7
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ **English** · [Français](README.fr.md)
2
+
1
3
  # @ddd-ts-kit/testing
2
4
 
3
5
  Readable tests without mocks: test data builders, given / when / then scenarios for
@@ -16,7 +18,124 @@ npm i -D @ddd-ts-kit/testing
16
18
  | `@ddd-ts-kit/testing/faker` | generated data for builders | `@faker-js/faker` (optional peer) |
17
19
  | `@ddd-ts-kit/testing/e2e` | HTTP client, `eventually`, `collectEvents` | Node only: no HTTP framework, no container |
18
20
  | `@ddd-ts-kit/testing/contracts` | contract suites of the kernel ports, `ContractSuite` to write another | the runner you pass |
19
- | `@ddd-ts-kit/testing/vitest` | `decorators()`, the Vite plugin that lets Vitest run standard decorators | Node only; runs the `tsc` of the project |
21
+ | `@ddd-ts-kit/testing/vitest` | `decorators()`, the Vite plugin that lets Vitest run standard decorators, and `decoratorCoverage()` | Node only; runs the `tsc` of the project |
22
+ | `@ddd-ts-kit/testing/coverage` | `governedCoverage()`, `governedJestCoverage()`, `unaffectedContexts()`: the coverage the runner measures and its floor, read from `ddd-ts-kit.config.json` | Node only |
23
+
24
+ The main entry, `/contracts` and `/e2e` follow the
25
+ [test levels](../../docs/ARCHITECTURE.md#7-test-levels); the builders serve every level, and the
26
+ other entries are set up once in the runner config:
27
+
28
+ ![The four test levels and what the package gives each one: given / when / then, givenEvents, givenProcess and expectRefusal for a domain spec; createTestBuses, InMemoryRepository and the execution context builders for an application spec; the contract suites for an adapter spec, run on the fake and on the real adapter; startTestServer, the HTTP client, expectProblem, collectEvents and eventually for an end-to-end spec. The same builders feed every level, and the vitest, coverage and faker entries are set up once in the runner config](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/testing-package-overview.svg)
29
+
30
+ ## A first test
31
+
32
+ One aggregate, its builder and two scenarios, in one file for the reading (a project gives the
33
+ aggregate its file under `domain/` and the builder its file under `test/builders/`):
34
+
35
+ <!-- checked sample -->
36
+ ```ts
37
+ import { AggregateRoot, ConceptError, type DomainEvent } from '@ddd-ts-kit/kernel';
38
+ import { Builder, given, resetSequences, sequence, type Defaults } from '@ddd-ts-kit/testing';
39
+ import { beforeEach, expect, it } from 'vitest';
40
+
41
+ // The model under test.
42
+ class OrderError extends ConceptError {
43
+ static alreadyPaid(orderId: string) {
44
+ return OrderError.refusal('ORDER_ALREADY_PAID', `Order ${orderId} is already paid.`, { orderId });
45
+ }
46
+ }
47
+
48
+ class OrderPaid implements DomainEvent {
49
+ readonly name = 'ordering.order.paid';
50
+
51
+ constructor(
52
+ readonly aggregateId: string,
53
+ readonly occurredAt: Date,
54
+ readonly amount: number,
55
+ ) {}
56
+ }
57
+
58
+ class Order extends AggregateRoot {
59
+ private paid = false;
60
+
61
+ private constructor(
62
+ id: string,
63
+ readonly amount: number,
64
+ ) {
65
+ super(id);
66
+ }
67
+
68
+ static place(id: string, amount: number): Order {
69
+ return new Order(id, amount);
70
+ }
71
+
72
+ get isPaid(): boolean {
73
+ return this.paid;
74
+ }
75
+
76
+ pay(at: Date): void {
77
+ if (this.paid) {
78
+ throw OrderError.alreadyPaid(this.id);
79
+ }
80
+
81
+ this.paid = true;
82
+ this.record(new OrderPaid(this.id, at, this.amount));
83
+ }
84
+ }
85
+
86
+ // The builder: valid defaults once, then methods named in the ubiquitous language.
87
+ const at = new Date('2026-03-01T10:00:00.000Z');
88
+
89
+ interface OrderProps {
90
+ readonly id: string;
91
+ readonly amount: number;
92
+ readonly paid: boolean;
93
+ }
94
+
95
+ class OrderBuilder extends Builder<OrderProps, Order> {
96
+ protected defaults(): Defaults<OrderProps> {
97
+ return { id: sequence('order'), amount: 30, paid: false };
98
+ }
99
+
100
+ protected construct(props: OrderProps): Order {
101
+ const order = Order.place(props.id, props.amount);
102
+
103
+ if (props.paid) {
104
+ order.pay(at);
105
+ }
106
+
107
+ return order;
108
+ }
109
+
110
+ paid(): this {
111
+ return this.with('paid', true);
112
+ }
113
+ }
114
+
115
+ const anOrder = (): OrderBuilder => new OrderBuilder();
116
+
117
+ // The specs: one intention, then the facts it recorded or the refusal.
118
+ beforeEach(() => {
119
+ resetSequences();
120
+ });
121
+
122
+ it('records the payment of a placed order', () => {
123
+ given(anOrder())
124
+ .when((order) => order.pay(at))
125
+ .thenEvents([{ name: 'ordering.order.paid', amount: 30 }]);
126
+ });
127
+
128
+ it('refuses to pay an order twice', () => {
129
+ given(anOrder().paid())
130
+ .when((order) => order.pay(at))
131
+ .thenError('ORDER_ALREADY_PAID', { orderId: 'order-1' })
132
+ .thenState((order) => expect(order.isPaid).toBe(true));
133
+ });
134
+ ```
135
+
136
+ No database, no container, no mock: the second test reads "given a paid order, when it is paid,
137
+ then it is refused with `ORDER_ALREADY_PAID`, and it is still paid". The sections below take
138
+ each piece in turn.
20
139
 
21
140
  ## Builders
22
141
 
@@ -71,6 +190,8 @@ const order = anOrder().paid().withLine(aLine().with('sku', 'sku-1')).build();
71
190
  const orders = anOrder().placedBy(aCustomer().named('Ada')).buildMany(3);
72
191
  ```
73
192
 
193
+ ![How a builder makes its value: the defaults declared once and what the test stated are merged by props(), which draws each sequence, lazy and generated value and builds each nested builder; build() hands the properties to construct, which goes through the public API and gives an aggregate valid by construction, and buildRequest(), a method the project writes on its own builder, gives the request payload of an end-to-end spec from the same properties](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/testing-package-builder.svg)
194
+
74
195
  For plain data (payloads, DTOs, simple value objects) the functional form is enough. It gives
75
196
  the generic `with(key, value)` and `withMany(partial)`, both checked against the properties:
76
197
 
@@ -97,15 +218,43 @@ aLine().with('quantity', 3).build(); // { sku: 'sku-1', quantity: 3 }
97
218
  - The class form keeps `with`, `withMany` and `adding` protected on purpose: the public
98
219
  surface of an aggregate builder is the language of the domain, not its property names.
99
220
 
221
+ Instead of creating the whole object in every test, where the one value the rule depends on is
222
+ lost among the others, and where a new argument of `Order.place` breaks every test:
223
+
224
+ ```ts
225
+ const order = Order.place('order-1', { id: 'customer-1', name: 'Grace Hopper' }, [{ sku: 'sku-1', quantity: 1 }]);
226
+ order.pay();
227
+ ```
228
+
229
+ write what the test is about, and let the builder hold the rest in one place:
230
+
231
+ ```ts
232
+ const order = anOrder().paid().build();
233
+ ```
234
+
100
235
  ## Generated data
101
236
 
102
237
  ```
103
238
  npm i -D @faker-js/faker
104
239
  ```
105
240
 
241
+ <!-- checked sample -->
106
242
  ```ts
243
+ import { defineBuilder, sequence } from '@ddd-ts-kit/testing';
107
244
  import { generated } from '@ddd-ts-kit/testing/faker';
108
245
 
246
+ // From the domain of the context.
247
+ interface CustomerProps {
248
+ readonly id: string;
249
+ readonly name: string;
250
+ readonly email: string;
251
+ readonly birthDate: Date;
252
+ }
253
+
254
+ declare class Customer {
255
+ static register(props: CustomerProps): Customer;
256
+ }
257
+
109
258
  export const aCustomer = defineBuilder<CustomerProps, Customer>({
110
259
  defaults: {
111
260
  id: sequence('customer'),
@@ -127,9 +276,29 @@ The defaults are generated yet valid: each generator is constrained to what the
127
276
  what the rule under test depends on.** A test never asserts a generated value it did not
128
277
  state, and never relies on a generated value falling on the right side of a rule.
129
278
 
279
+ Instead of leaving the value the rule is about to the generator, so that the test passes
280
+ because of a default nobody reads:
281
+
282
+ ```ts
283
+ it('lets an adult buy wine', () => {
284
+ const customer = aCustomer().build(); // adult because the default says so today
285
+ expect(customer.mayBuy(wine, today)).toBe(true);
286
+ });
287
+ ```
288
+
289
+ state it, with a date the reader can check:
290
+
291
+ ```ts
292
+ it('lets an adult buy wine', () => {
293
+ const customer = aCustomer().with('birthDate', new Date('2008-05-01')).build();
294
+ expect(customer.mayBuy(wine, new Date('2026-05-01'))).toBe(true); // 18 on that day
295
+ });
296
+ ```
297
+
130
298
  Every `generated` value is drawn from one generator per test file, `dataGenerator()`. Its seed
131
299
  comes from `TEST_SEED`, else it is random, and it is always known (`dataGenerator().seed`):
132
300
 
301
+ <!-- checked sample -->
133
302
  ```ts
134
303
  // vitest setup file
135
304
  import { beforeEach, onTestFailed } from 'vitest';
@@ -237,6 +406,37 @@ error, and fails with an `AssertionError` that names what was refused instead, o
237
406
  that differ, one line each. The facts given must be carried with those values; the refusal may
238
407
  carry others.
239
408
 
409
+ Instead of asserting the wording of a message, which breaks when it is rephrased, or the class
410
+ of the error, which says nothing about which refusal it was:
411
+
412
+ ```ts
413
+ expect(() => order.pay(at)).toThrow('Order order-1 is already paid.');
414
+ expect(() => order.pay(at)).toThrow(OrderError);
415
+ ```
416
+
417
+ assert the code, stable for every client, and the facts the refusal carries:
418
+
419
+ <!-- checked sample -->
420
+ ```ts
421
+ import { ConceptError } from '@ddd-ts-kit/kernel';
422
+ import { expectRefusal } from '@ddd-ts-kit/testing';
423
+
424
+ class OrderError extends ConceptError {
425
+ static alreadyPaid(orderId: string) {
426
+ return OrderError.refusal('ORDER_ALREADY_PAID', `Order ${orderId} is already paid.`, { orderId });
427
+ }
428
+ }
429
+
430
+ const order = {
431
+ id: 'order-1',
432
+ pay(): void {
433
+ throw OrderError.alreadyPaid(this.id);
434
+ },
435
+ };
436
+
437
+ expectRefusal(() => order.pay(), 'ORDER_ALREADY_PAID', { orderId: 'order-1' });
438
+ ```
439
+
240
440
  ## Handlers
241
441
 
242
442
  ```ts
@@ -271,6 +471,8 @@ The handler only saves: `await this.orders.save(order); return order.id;`. `disp
271
471
  gives the fake what the application gives every repository: once `save` (or any method handed
272
472
  the aggregate) resolved, the events the aggregate recorded are handed over to `domainEvents`.
273
473
 
474
+ ![A handler spec on createTestBuses: the spec executes a command on the command bus, which runs it in the unit of work; the real handler saves into the repository fake wrapped by dispatching, which keeps a copy while the recorded events wait; once the handler has returned, domainEvents hands the events to a before-commit handler, which publishes an integration event on the in-memory event bus; when execute has resolved, the spec reads the state of the fake and the published events](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/testing-package-handler-spec.svg)
475
+
274
476
  A handler spec has the production timing. `commandBus` runs every command in `unitOfWork`
275
477
  (an `InMemoryUnitOfWork`), as the bus of the application does: the events of every save wait
276
478
  until the command handler has returned, then are dispatched in recording order, and
@@ -295,6 +497,88 @@ copy of what is saved (through `snapshots({ toSnapshot, fromSnapshot })` or `{ c
295
497
  enforces optimistic concurrency on `version`, raising `ConcurrencyError` (code
296
498
  `CONCURRENCY_CONFLICT`). Assert its state with `all()` and `count()`.
297
499
 
500
+ ### A fake, not a mock
501
+
502
+ Instead of a mock, which checks the calls the handler made and nothing of their effect:
503
+
504
+ ```ts
505
+ const orders = { findById: vi.fn(), save: vi.fn() };
506
+ await new PlaceOrderHandler(orders, ids, clock).execute(new PlaceOrder('sku-1'));
507
+ expect(orders.save).toHaveBeenCalledTimes(1); // still green if the order could never be read back
508
+ ```
509
+
510
+ run the real handler through the bus on a fake, and assert what is true afterwards:
511
+
512
+ <!-- checked sample -->
513
+ ```ts
514
+ import { AggregateRoot, Command, Repository, type Clock, type CommandHandler, type IdGenerator } from '@ddd-ts-kit/kernel';
515
+ import { createTestBuses, InMemoryRepository, snapshots } from '@ddd-ts-kit/testing';
516
+ import { expect, it } from 'vitest';
517
+
518
+ // From the domain of the context.
519
+ interface OrderSnapshot {
520
+ readonly id: string;
521
+ readonly sku: string;
522
+ }
523
+
524
+ declare class Order extends AggregateRoot {
525
+ static place(id: string, sku: string, at: Date): Order;
526
+ static rehydrate(snapshot: OrderSnapshot): Order;
527
+ toSnapshot(): OrderSnapshot;
528
+ }
529
+
530
+ abstract class OrderRepository extends Repository<Order> {}
531
+
532
+ // From the application layer: the use case under test.
533
+ class PlaceOrder extends Command<string> {
534
+ constructor(readonly sku: string) {
535
+ super();
536
+ }
537
+ }
538
+
539
+ class PlaceOrderHandler implements CommandHandler<PlaceOrder> {
540
+ constructor(
541
+ private readonly orders: OrderRepository,
542
+ private readonly ids: IdGenerator,
543
+ private readonly clock: Clock,
544
+ ) {}
545
+
546
+ async execute(command: PlaceOrder): Promise<string> {
547
+ const order = Order.place(this.ids.next(), command.sku, this.clock.now());
548
+
549
+ await this.orders.save(order);
550
+
551
+ return order.id;
552
+ }
553
+ }
554
+
555
+ // From test/fakes/: a working repository, held to the same contract suite as the adapter.
556
+ class InMemoryOrderRepository extends InMemoryRepository<Order> implements OrderRepository {
557
+ constructor() {
558
+ super(snapshots({ toSnapshot: (order) => order.toSnapshot(), fromSnapshot: Order.rehydrate }));
559
+ }
560
+ }
561
+
562
+ it('places an order', async () => {
563
+ const { commandBus, dispatching, domainEvents, ids, clock } = createTestBuses();
564
+ const orders = dispatching(new InMemoryOrderRepository());
565
+ const heard: string[] = [];
566
+
567
+ domainEvents.on('ordering.order.placed', async (event) => void heard.push(event.aggregateId));
568
+ commandBus.register(PlaceOrder, new PlaceOrderHandler(orders, ids, clock));
569
+
570
+ const id = await commandBus.execute(new PlaceOrder('sku-1'));
571
+
572
+ expect((await orders.findById(id))?.toSnapshot()).toEqual({ id, sku: 'sku-1' });
573
+ expect(heard).toEqual([id]);
574
+ });
575
+ ```
576
+
577
+ The mock breaks when the handler calls another method of the port for the same effect, and stays
578
+ green when the saved order cannot be found again; the fake does neither. The
579
+ [`test/no-mocks`](../eslint-plugin/docs/rules/test-no-mocks.md) rule reports `vi.fn`,
580
+ `vi.mock` and their kin in the tests of a context.
581
+
298
582
  ### Who is acting
299
583
 
300
584
  `createTestBuses()` also gives `executionScope` and `actorContext`, the two doubles of the
@@ -329,9 +613,16 @@ The whole application, real adapters included, driven from the outside: requests
329
613
  and integration events out. Nothing here knows the HTTP framework or how the application is
330
614
  composed; `start` says it.
331
615
 
616
+ <!-- checked sample -->
332
617
  ```ts
333
- import { createServer } from 'node:http';
334
- import { collectEvents, expectProblem, expectStatus, listenOnRandomPort, startTestServer } from '@ddd-ts-kit/testing/e2e';
618
+ import { createServer, type RequestListener } from 'node:http';
619
+ import { listenOnRandomPort, startTestServer } from '@ddd-ts-kit/testing/e2e';
620
+ import { afterAll } from 'vitest';
621
+
622
+ // From the project: its composition root, its HTTP framework, a token of the test identity provider.
623
+ declare function composeApplication(): object;
624
+ declare function expressApp(app: object): RequestListener;
625
+ declare const testToken: string;
335
626
 
336
627
  const app = composeApplication(); // the composition root of main.ts
337
628
  const server = await startTestServer({
@@ -374,8 +665,10 @@ const { body: order } = expectStatus(await client.post<OrderView>('/orders', anO
374
665
  **Reset between tests.** The harness names the contract and leaves the implementation to the
375
666
  project and its driver:
376
667
 
668
+ <!-- checked sample -->
377
669
  ```ts
378
670
  import type { DatabaseReset } from '@ddd-ts-kit/testing/e2e';
671
+ import { beforeEach } from 'vitest';
379
672
 
380
673
  // PostgreSQL, with any client exposing `query`: every table of the application schemas.
381
674
  export function truncateAllTables(db: { query(sql: string): Promise<{ rows: { name: string }[] }> }, schemas: string[]): DatabaseReset {
@@ -392,13 +685,16 @@ export function truncateAllTables(db: { query(sql: string): Promise<{ rows: { na
392
685
  };
393
686
  }
394
687
 
688
+ declare const database: DatabaseReset; // truncateAllTables(pool, ['ordering', 'billing']), made once
689
+
395
690
  beforeEach(() => database.reset());
396
691
  ```
397
692
 
398
693
  ## Three levels with the same vocabulary
399
694
 
400
- One rule, "an order that is already paid cannot be paid again", at three of the four test
401
- levels of `docs/ARCHITECTURE.md` (the adapter level is the contract suites, below). The builder is the same; only what runs underneath changes.
695
+ One rule, "an order that is already paid cannot be paid again", at three of the four
696
+ [test levels](../../docs/ARCHITECTURE.md#7-test-levels) (the adapter level is the contract
697
+ suites, below). The builder is the same; only what runs underneath changes.
402
698
 
403
699
  ```ts
404
700
  // 1. Domain: the aggregate alone. Milliseconds, no double.
@@ -445,11 +741,35 @@ again.
445
741
  A fake is only worth trusting if it behaves like the adapter it stands for. Each suite takes
446
742
  the runner's functions and a factory, and is run against both.
447
743
 
744
+ ![One contract suite, two subjects: repositoryContract runs the same tests against the in-memory fake, which the handler specs use, and against the Drizzle adapter on a real database, which production uses; both implement the repository port of the domain, so what a handler spec proves on the fake holds for production](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/testing-package-contract-suite.svg)
745
+
746
+ <!-- checked sample -->
448
747
  ```ts
449
748
  // contexts/ordering/test/integration/order-repository.contract.spec.ts
450
749
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
750
+ import type { AggregateRoot, Repository } from '@ddd-ts-kit/kernel';
751
+ import type { InMemoryRepository } from '@ddd-ts-kit/testing';
451
752
  import { repositoryContract } from '@ddd-ts-kit/testing/contracts';
452
753
 
754
+ // From the context and its test folder.
755
+ interface Line {
756
+ readonly sku: string;
757
+ readonly quantity: number;
758
+ }
759
+
760
+ declare class Order extends AggregateRoot {
761
+ addLine(line: Line): void;
762
+ toSnapshot(): object;
763
+ }
764
+
765
+ declare const anOrder: () => { build(): Order };
766
+ declare const aLine: () => { build(): Line };
767
+ // The fake of the Handlers section: it extends InMemoryRepository, it never extends the port.
768
+ declare class InMemoryOrderRepository extends InMemoryRepository<Order> {
769
+ constructor();
770
+ }
771
+ declare function freshDrizzleRepository(): Promise<Repository<Order>>;
772
+
453
773
  const runner = { describe, it, expect, beforeEach, afterEach };
454
774
  const options = {
455
775
  createAggregate: () => anOrder().build(),
@@ -484,8 +804,8 @@ repositoryContract(runner, { ...options, name: 'DrizzleOrderRepository', createR
484
804
  | `fileStorageContract` | any `FileStorage`: content round trip with size, media type and checksum, several mebibytes streamed, a write beyond `maxBytes` refused leaving the previous content, keys that could leave their place refused, deletion, copy, move, listing by prefix, signed download and upload links that expire, refuse a changed signature or another media type, and are never signed for more than an hour, a download served as an attachment even without a name. `follow` sends a request to a link: `fetch` by default, `followServed(storage)` for a storage the application serves |
485
805
  | `rateLimiterContract` | any `RateLimiter`: limit, `remaining` and `retryAfterMs`, refused calls not counted, sliding window, isolation of keys, concurrent calls |
486
806
 
487
- The cache, lock and rate limiter suites also take `elapse(ms)`, which makes time pass for the
488
- subject: advance the clock it reads, or really wait when the time is a server's. The job
807
+ The cache, lock, rate limiter and file storage suites also take `elapse(ms)`, which makes time
808
+ pass for the subject: advance the clock it reads, or really wait when the time is a server's. The job
489
809
  scheduler suite takes it from `createScheduler`, with the scheduler and the scope it runs its
490
810
  jobs in: `{ scheduler, scope, elapse }`. It asks for a few seconds at most, with jobs of every
491
811
  second.
@@ -507,9 +827,15 @@ runner is the `ContractRunner` type.
507
827
  A package that holds a port of its own publishes its contract on the same base, `ContractSuite`:
508
828
  a subject created anew before each test, and `it`, `expect`, `afterEach` bound to the runner.
509
829
 
830
+ <!-- checked sample -->
510
831
  ```ts
511
832
  import { ContractSuite, type Awaitable, type ContractRunner } from '@ddd-ts-kit/testing/contracts';
512
833
 
834
+ // The port the package holds.
835
+ export interface Geocoder {
836
+ locate(address: string): Promise<{ readonly latitude: number; readonly longitude: number } | undefined>;
837
+ }
838
+
513
839
  export function geocoderContract(runner: ContractRunner, options: { name: string; create(): Awaitable<Geocoder> }): void {
514
840
  new GeocoderSuite(runner, () => options.create()).describe(`${options.name} (geocoder contract)`);
515
841
  }
@@ -531,8 +857,14 @@ shows through `JSON.stringify`, an object spread, the values `Object.keys` leads
531
857
  `util.inspect` at any depth, hidden properties included. The failure names the view and the
532
858
  rank of the secret, never its value.
533
859
 
860
+ <!-- checked sample -->
534
861
  ```ts
862
+ import { KafkaEventBus } from '@ddd-ts-kit/kafka';
535
863
  import { expectNoSecret } from '@ddd-ts-kit/testing';
864
+ import { it } from 'vitest';
865
+
866
+ declare const brokers: string;
867
+ declare const password: string;
536
868
 
537
869
  it('shows no credential', () => {
538
870
  const bus = new KafkaEventBus({ brokers, client: { 'sasl.password': password } });
@@ -547,6 +879,7 @@ Vitest 5 (Vite 8, Oxc) lowers legacy decorators and does not lower standard (TC3
547
879
  spec that imports a `.ts` file holding `@CommandHandler(...)` or `@Retry(...)` stops on a
548
880
  syntax error. `decorators()` closes that gap, and only that one.
549
881
 
882
+ <!-- checked sample -->
550
883
  ```ts
551
884
  // vitest.config.ts
552
885
  import { defineConfig } from 'vitest/config';
@@ -588,6 +921,7 @@ It does not type-check (the build does), and it does not touch `.tsx`.
588
921
  to, read from the `coverage` key of `ddd-ts-kit.config.json` (the reference when the key is
589
922
  absent), so that no threshold is written in a runner config:
590
923
 
924
+ <!-- checked sample -->
591
925
  ```ts
592
926
  // vitest.config.ts
593
927
  import { governedCoverage } from '@ddd-ts-kit/testing/coverage';
@@ -603,6 +937,11 @@ export default defineConfig(({ mode }) => ({
603
937
  measures `infrastructure`, any other mode the four.
604
938
  - `thresholds`: one entry per context and layer that holds a source file, so a context cannot
605
939
  hide behind another and an empty layer is held to nothing until its first file.
940
+ - A partial run measures and holds only the contexts named in `DDD_TS_KIT_AFFECTED` (comma
941
+ separated, as [`ddd-ts-kit affected`](../cli/README.md#affected) computes them for a pull
942
+ request); unset, the run covers every context. `unaffectedContexts()` answers the directories
943
+ of the other contexts, ending in `/`, for the runner to exclude their specs:
944
+ ``exclude: unaffectedContexts().map((directory) => `${directory}**`)``.
606
945
  - `governedJestCoverage()` gives the same as `collectCoverageFrom` and `coverageThreshold`.
607
946
  - `REFERENCE_COVERAGE` is the reference: domain 90/85/90/90, application 85/80/85/85,
608
947
  presentation 80/70/80/80, infrastructure 70/60/70/70 (statements, branches, functions, lines).
@@ -611,3 +950,13 @@ Under the legacy decorator mode with `emitDecoratorMetadata`, add `decoratorCove
611
950
  `@ddd-ts-kit/testing/vitest` to `plugins`: the statements the transform writes to apply
612
951
  decorators hold, for every type a decorated class names, a branch no test can take; the plugin
613
952
  marks them with the hint the `v8` provider reads, as the provider does by itself for SWC.
953
+
954
+ ## See also
955
+
956
+ - [Testing strategy](../../docs/concepts/testing-strategy.md): why fakes and not mocks, the four
957
+ levels, what to test where
958
+ - [Test runners](../../docs/tools/test-runners.md): Vitest or Jest, and what each gives with the kit
959
+ - [`@ddd-ts-kit/di`, tests](../di/README.md#tests): `createTestApplication` and `fake`, the
960
+ application spec with the container
961
+ - [`@ddd-ts-kit/kernel`](../kernel/README.md): the aggregates, ports and in-memory adapters the
962
+ builders and fakes work with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/testing",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Readable tests without mocks: test data builders, given/when/then scenarios and contract suites shared by fakes and real adapters.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,13 +46,13 @@
46
46
  "dist"
47
47
  ],
48
48
  "dependencies": {
49
- "@ddd-ts-kit/kernel": "0.15.0"
49
+ "@ddd-ts-kit/kernel": "0.16.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@ddd-ts-kit/aspects": "0.15.0",
53
- "@ddd-ts-kit/events": "0.15.0",
52
+ "@ddd-ts-kit/aspects": "0.16.1",
53
+ "@ddd-ts-kit/events": "0.16.1",
54
54
  "@faker-js/faker": "^10.6.0",
55
- "@ddd-ts-kit/scheduling": "0.15.0"
55
+ "@ddd-ts-kit/scheduling": "0.16.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@faker-js/faker": ">=9.0.0"