@kensio/part-factory-test-data 1.3.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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
+ "name": "part-factory-test-data",
4
+ "version": "1.3.0",
5
+ "description": "How to build test data with @kensio/part-factory: keeping in the factory everything a test does not care about, passing dependencies at call time so factories stay independent and shareable, and choosing between its static, dynamic, variant and mapped factories.",
6
+ "author": {
7
+ "name": "Kensio Software",
8
+ "email": "hugh@kensiosoftware.co.uk"
9
+ },
10
+ "homepage": "https://kensio.ai",
11
+ "repository": "https://github.com/KensioSoftware/kensio.ai",
12
+ "license": "Apache-2.0",
13
+ "keywords": ["testing", "test-data", "factory", "part-factory", "faker"]
14
+ }
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @kensio/part-factory-test-data
2
+
3
+ A Claude Code skill for building test data with [Part Factory](https://partfactory.dev/)
4
+ (`@kensio/part-factory`), a small typed object factory library.
5
+
6
+ The package README is the authority on the API. This skill covers which factory to reach for, when a
7
+ mapped factory earns its place, and where factories should live.
8
+
9
+ ## Install
10
+
11
+ From the marketplace:
12
+
13
+ ```bash
14
+ claude plugin marketplace add KensioSoftware/kensio.ai
15
+ claude plugin install part-factory-test-data@kensio
16
+ ```
17
+
18
+ From npm:
19
+
20
+ ```bash
21
+ npm install @kensio/part-factory-test-data
22
+ ```
23
+
24
+ ## What it covers
25
+
26
+ **Which factory to reach for.** `StaticFactory` for fixed defaults. `DynamicFactory` when the
27
+ defaults have to be generated fresh, paired with [faker](https://fakerjs.dev/), which is what gives
28
+ tests their isolation. `VariantFactory` for a named variation of a base factory, when the variation
29
+ is worth a name in the test. `MappedFactory` when the output shape differs from the parts you want
30
+ to override.
31
+
32
+ **When a mapped factory earns its place.** The map should be a real transformation: parts to an
33
+ encoded form body, or front matter parts to a file as written on disk. If the map is copying fields
34
+ across into an object of the same shape, you wanted a `DynamicFactory`.
35
+
36
+ **Do not wrap a factory in a function that applies overrides.** `make(overrides)` already is that
37
+ function, and it does the job better, since its overrides are partial all the way down the nested
38
+ structure where a spread replaces a nested object whole. A wrapper doing more than passing overrides
39
+ through is a signal that the factory is the wrong shape, or that the output type is fighting you.
40
+
41
+ **Factories belong beside the type they construct.** A library defining an event or message shape
42
+ should export a factory for it, so consumers never hand-roll the literal. The example the skill
43
+ works through is an AWS Lambda function URL invocation event in payload format 2.0: around thirty
44
+ lines of which two matter to any test. Hand-writing it in three files gives three copies that drift,
45
+ and it carries a trap, because the path is in the event twice, as `rawPath` and as
46
+ `requestContext.http.path`. A test that sets one and not the other passes against a handler reading
47
+ the field the test set, and fails in production against the same handler reading the other one. A
48
+ `MappedFactory` sets it once.
49
+
50
+ ## Related skills
51
+
52
+ - [`isolated-testing-style`](https://github.com/KensioSoftware/kensio.ai/tree/main/plugins/isolated-testing-style)
53
+ is the general argument for keeping setup out of test bodies, and for taking isolation from
54
+ randomised data.
55
+ - [`yulin-aws-simulation`](https://github.com/KensioSoftware/kensio.ai/tree/main/plugins/yulin-aws-simulation)
56
+ covers the AWS simulator those tests run against.
57
+
58
+ Part of [kensio.ai](https://github.com/KensioSoftware/kensio.ai). Licensed under the Apache License
59
+ 2.0. See the [LICENSE](https://github.com/KensioSoftware/kensio.ai/blob/main/LICENSE) in the
60
+ repository root.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@kensio/part-factory-test-data",
3
+ "version": "1.3.0",
4
+ "description": "How to build test data with @kensio/part-factory: keeping in the factory everything a test does not care about, passing dependencies at call time so factories stay independent and shareable, and choosing between its static, dynamic, variant and mapped factories.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "claude-code-plugin",
9
+ "factory",
10
+ "faker",
11
+ "kensio",
12
+ "part-factory",
13
+ "skill",
14
+ "test-data",
15
+ "testing"
16
+ ],
17
+ "homepage": "https://kensio.ai",
18
+ "license": "Apache-2.0",
19
+ "author": "Kensio Software <hugh@kensiosoftware.co.uk>",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/KensioSoftware/kensio.ai.git",
23
+ "directory": "plugins/part-factory-test-data"
24
+ },
25
+ "files": [
26
+ ".claude-plugin",
27
+ "skills",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: part-factory-test-data
3
+ description: Build test data with @kensio/part-factory, keeping in the factory everything a test does not care about, passing dependencies at call time so factories stay independent, and choosing between StaticFactory, DynamicFactory, VariantFactory, MappedFactory and AsyncMappedFactory. Use when writing test fixtures or builders, when a test file is full of object literals, when a shared event, message or payload shape is being hand-written, when a required field is about to be made optional to ease test setup, and when tempted to wrap a factory in a helper function that applies overrides.
4
+ ---
5
+
6
+ # Building test data with Part Factory
7
+
8
+ [Part Factory](https://partfactory.dev/) (`@kensio/part-factory`) builds typed objects for tests: a
9
+ factory holds the defaults, and `make(overrides)` returns an object with the overrides applied down
10
+ through the nested structure.
11
+
12
+ The package README is the authority on the API. This skill covers what to put in a factory, where
13
+ factories should live, and which one to reach for.
14
+
15
+ It serves the `isolated-testing-style` skill. Factories are what make building state inside each
16
+ test cheap enough that nobody reaches for a shared fixture in the first place.
17
+
18
+ ## Say only what the test is about
19
+
20
+ A factory defines every value the test does not care about, so the test can state only the values it
21
+ does. That is the whole point of one.
22
+
23
+ Without it, a test opens with twenty lines of construction and it is not clear which of them the
24
+ assertions actually depend on. With it, the lines that are there are the lines that matter:
25
+
26
+ ```typescript
27
+ it("charges VAT on the order total", () => {
28
+ // Given an order whose lines add up to a total the assertion depends on.
29
+ const order = orderFactory.make({ lines: [{ price: 1000 }, { price: 2000 }] });
30
+
31
+ // When it is priced.
32
+ const priced = priceOrder(order);
33
+
34
+ // Then VAT is a fifth of that total.
35
+ expect(priced.vat).toBe(600);
36
+ });
37
+ ```
38
+
39
+ The prices are written down because the assertion is arithmetic on them. The order id, the customer
40
+ id and the dates are not, because the test would read the same whatever they were. The rule is that
41
+ simple: **if an assertion depends on a value, put it in the test; otherwise let the factory supply
42
+ it.**
43
+
44
+ This also removes a pressure that quietly damages production types. When constructing an object by
45
+ hand is painful, the tempting fix is to mark its required fields optional so tests can skip them —
46
+ weakening the type for every caller in order to serve the tests. A factory makes the setup cheap, so
47
+ the type can go on saying what is actually required.
48
+
49
+ ## Overrides are a deep partial
50
+
51
+ Overrides merge into the defaults rather than replacing them, all the way down the nested structure.
52
+ That is what makes "state only what matters" possible: a test can set one field three levels deep
53
+ and leave its siblings alone.
54
+
55
+ Two consequences worth knowing:
56
+
57
+ - Arrays override by index, so `{ lines: [{ price: 1000 }] }` replaces the first default line and
58
+ leaves any others in place.
59
+ - An empty array does not clear the defaults. To assert on emptiness, build the case some other way
60
+ rather than expecting `{ lines: [] }` to do it.
61
+
62
+ ## Which factory
63
+
64
+ - **`StaticFactory`** when the defaults are fixed values. The simplest thing that works, and the
65
+ right default choice.
66
+ - **`DynamicFactory`** when the defaults have to be generated fresh for each object. Pair it with
67
+ [`@faker-js/faker`](https://fakerjs.dev/). This is what gives tests their isolation: a random
68
+ email or a UUID means two tests cannot collide, so neither needs tearing down.
69
+ - **`VariantFactory`** for a named variation of a base factory, when the variation is a concept the
70
+ tests talk about. `closedOfferFactory` reads better in ten tests than
71
+ `offerFactory.make({ closesAt: aMinuteAgo })` written ten times.
72
+ - **`MappedFactory`** when the output shape differs from the parts you want to override.
73
+ - **`AsyncMappedFactory`** when producing the value means awaiting something, such as inserting a
74
+ row or signing a payload.
75
+
76
+ ```typescript
77
+ import { DynamicFactory } from "@kensio/part-factory";
78
+ import { faker } from "@faker-js/faker";
79
+
80
+ export const customerFactory = new DynamicFactory<Customer>(() => ({
81
+ id: faker.string.uuid(),
82
+ email: faker.internet.email(),
83
+ name: faker.person.fullName(),
84
+ }));
85
+ ```
86
+
87
+ ## Do not add a variant for every case
88
+
89
+ A variant earns its name when several tests mean the same thing by it. One test that needs an
90
+ unusual value should write that value down, not gain a factory of its own in a file somewhere else.
91
+
92
+ The failure mode is a directory of `cancelledOrderWithRefundAndNoAddressFactory` names, each used
93
+ once, where reading a test means going to find out what its factory actually sets. Writing the field
94
+ in the test is shorter and says more.
95
+
96
+ ## Reach for MappedFactory only when the map is a real transformation
97
+
98
+ `MappedFactory` earns its place when the thing you want to override is not shaped like the thing you
99
+ want back. Good cases:
100
+
101
+ - Parts to an encoded form body: `{ email, password }` mapped to a
102
+ `application/x-www-form-urlencoded` string.
103
+ - Front matter parts to a file as written on disk: `{ title, tags, body }` mapped to the YAML block
104
+ plus the markdown underneath.
105
+ - Components to a formatted identifier: ARN parts mapped to the ARN string.
106
+
107
+ If the mapping function is copying fields across into an object of the same shape, you wanted a
108
+ `DynamicFactory`. An identity map adds a type parameter, a second function and a layer of
109
+ indirection, and buys nothing.
110
+
111
+ ## Pass dependencies at call time
112
+
113
+ A factory that needs something from the outside world — a store, a client, a configured host —
114
+ declares it as a third type parameter and receives it as the second argument to `make`:
115
+
116
+ ```typescript
117
+ export const storedOrderFactory = new AsyncMappedFactory<
118
+ OrderParts,
119
+ Order,
120
+ { orders: OrderStore }
121
+ >(
122
+ () => ({ total: faker.number.int({ min: 100, max: 10_000 }) }),
123
+ async (parts, { orders }) => orders.insert({ id: faker.string.uuid(), ...parts }),
124
+ );
125
+
126
+ // In a test, which decides what to hand it.
127
+ const order = await storedOrderFactory.make({ total: 5000 }, { orders });
128
+ ```
129
+
130
+ Dependencies are given at call time rather than held by the factory, and are used as they are given,
131
+ never fetched or awaited. That is what keeps a factory shareable: it holds no state of its own,
132
+ reaches for nothing ambient, and cannot depend on what another factory did first. A factory built
133
+ this way can be used in every test file in the codebase and still stand on its own.
134
+
135
+ Keep each dependency as narrow as the factory actually needs. An `OrderStore` is a dependency; the
136
+ whole application is not. A wide dependency is how state starts leaking between tests that were
137
+ supposed to be independent.
138
+
139
+ ## Do not wrap a factory in a function that applies overrides
140
+
141
+ ```typescript
142
+ // Anti-pattern. This is make(overrides) with extra steps.
143
+ export function makeCustomer(overrides: Partial<Customer> = {}): Customer {
144
+ return { ...customerFactory.make(), ...overrides };
145
+ }
146
+ ```
147
+
148
+ `make(overrides)` already is that function, and it does the job better: its overrides are partial
149
+ all the way down the nested structure, where the spread above replaces a nested object whole.
150
+
151
+ A wrapper that does anything more than pass overrides through is a signal to read rather than to
152
+ write. It usually means one of two things:
153
+
154
+ - **The factory is the wrong shape.** The wrapper is computing something the defaults should be
155
+ computing, or deriving one field from another. Move that into a `DynamicFactory` defaults
156
+ function, which receives the overrides, or into a `MappedFactory` map.
157
+ - **The output type is fighting you.** The wrapper is casting, widening or filling in a field the
158
+ type demands but the test does not care about. Fix the type, or use `MappedFactory` so the parts
159
+ and the output are allowed to differ.
160
+
161
+ The same goes for a wrapper that exists to pass a dependency: declare it on the factory instead.
162
+
163
+ ## Factories belong beside the type they construct
164
+
165
+ A library that defines an event, a message or a payload shape should export a factory for it.
166
+ Otherwise every consumer hand-rolls the literal, and every copy drifts.
167
+
168
+ The worked example: an AWS Lambda function URL invocation event, payload format 2.0. It is around
169
+ thirty lines, of which two matter to any given test.
170
+
171
+ ```json
172
+ {
173
+ "version": "2.0",
174
+ "routeKey": "$default",
175
+ "rawPath": "/upload",
176
+ "rawQueryString": "part=3",
177
+ "headers": { "host": "abc.lambda-url.eu-west-2.on.aws", "user-agent": "..." },
178
+ "queryStringParameters": { "part": "3" },
179
+ "cookies": ["session=abc"],
180
+ "requestContext": {
181
+ "http": { "method": "POST", "path": "/upload", "sourceIp": "127.0.0.1" }
182
+ },
183
+ "body": "...",
184
+ "isBase64Encoded": false
185
+ }
186
+ ```
187
+
188
+ Hand-writing that in three test files gives three copies that drift as the shape changes. It also
189
+ carries a real trap: the path is in the event twice, as `rawPath` and as `requestContext.http.path`,
190
+ and the query string is in it twice, as `rawQueryString` and as `queryStringParameters`. A test that
191
+ sets one and not the other passes against a handler reading the field the test set, and fails in
192
+ production against the same handler reading the other one.
193
+
194
+ A `MappedFactory` removes both problems. The parts are what a test cares about, and the map is what
195
+ fills the event in consistently:
196
+
197
+ ```typescript
198
+ import { MappedFactory } from "@kensio/part-factory";
199
+
200
+ interface FunctionUrlRequestParts {
201
+ method: string;
202
+ path: string;
203
+ query: Record<string, string>;
204
+ body?: string;
205
+ }
206
+
207
+ export const functionUrlEventFactory = new MappedFactory<
208
+ FunctionUrlRequestParts,
209
+ FunctionUrlEvent
210
+ >(
211
+ () => ({ method: "GET", path: "/", query: {} }),
212
+ (parts) => ({
213
+ version: "2.0",
214
+ routeKey: "$default",
215
+ rawPath: parts.path,
216
+ rawQueryString: new URLSearchParams(parts.query).toString(),
217
+ queryStringParameters: parts.query,
218
+ // ... and the rest, filled in once.
219
+ requestContext: {
220
+ http: { method: parts.method, path: parts.path, sourceIp: "127.0.0.1" },
221
+ },
222
+ body: parts.body,
223
+ isBase64Encoded: false,
224
+ }),
225
+ );
226
+
227
+ // In a test, the two lines that matter are the two lines written.
228
+ const event = functionUrlEventFactory.make({ method: "POST", path: "/upload" });
229
+ ```
230
+
231
+ The path can no longer disagree with itself, because there is one place it is set.
232
+
233
+ Export the factory from the package that owns the type, from a test-support entry point so it does
234
+ not ship in the production bundle. Consumers then get a correct event with one call, and a change to
235
+ the shape is made once.