@kensio/isolated-testing-style 1.3.0 → 1.6.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "isolated-testing-style",
4
- "version": "1.3.0",
4
+ "version": "1.6.0",
5
5
  "description": "An opinionated testing style: start from given/when/then, use real collaborators through simulation instead of stubs, take isolation from randomised data instead of shared setup and teardown, and assert behaviour instead of call counts.",
6
6
  "author": {
7
7
  "name": "Kensio Software",
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # @kensio/isolated-testing-style
2
2
 
3
- An opinionated testing style, packaged as a Claude Code skill. Real collaborators through simulation
4
- instead of stubs, isolation from randomised data instead of setup and teardown, and assertions on
5
- behaviour instead of call counts.
3
+ An opinionated testing style, packaged as a Claude Code skill. Real collaborators through
4
+ simulation, isolation from randomised data, and assertions on behaviour.
6
5
 
7
6
  Every rule in it comes from a specific failure it would have caught, and the failure is written down
8
7
  next to the rule.
@@ -26,27 +25,27 @@ npm install @kensio/isolated-testing-style
26
25
 
27
26
  **Prefer real collaborators through simulation.** A stub asserts that your code called something. A
28
27
  simulator asserts that it called the service correctly. A stub answers whatever you told it to
29
- answer, so it agrees with your understanding of the API by construction, and cannot find the case
30
- where that understanding is wrong.
28
+ answer. It agrees with your understanding of the API by construction, and cannot find the case where
29
+ that understanding is wrong.
31
30
 
32
31
  **Get isolation from the data, not from setup and teardown.** Randomised values from faker mean two
33
- tests cannot collide, so there is nothing to tear down and no ordering to depend on. A shared
32
+ tests cannot collide. There is nothing to tear down and no ordering to depend on. A shared
34
33
  environment built the way production is built is closer to production than a minimal one rebuilt per
35
34
  test, and it is faster.
36
35
 
37
36
  **Assert behaviour, not call counts.** To prove a value is cached, delete the underlying resource
38
37
  and show the cached value survives. To prove a retry, make the first call fail and the second
39
- succeed. Both hold however the code is implemented, which is what makes refactoring safe.
38
+ succeed. Both hold however the code is implemented. That is what makes refactoring safe.
40
39
 
41
40
  **Never pin a value computed by the code under test.** Pinning a hash you generated by running the
42
41
  same function only proves the function is deterministic. It keeps passing after the function becomes
43
42
  wrong. Pin against an independent authority, or let a real implementation validate it.
44
43
 
45
44
  **Put test support beside the code.** Factories and helpers live in a test-support module next to
46
- what they support, so test files hold tests.
45
+ what they support. Test files hold tests.
47
46
 
48
- **Comment test bodies with Given, When and Then**, saying why the lines are there rather than
49
- restating them.
47
+ **Comment test bodies with Given, When and Then**, saying why the lines are there, not restating
48
+ them.
50
49
 
51
50
  ## The other two skills
52
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/isolated-testing-style",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "description": "An opinionated testing style: start from given/when/then, use real collaborators through simulation instead of stubs, take isolation from randomised data instead of shared setup and teardown, and assert behaviour instead of call counts.",
5
5
  "keywords": [
6
6
  "claude",
@@ -6,7 +6,7 @@ description: Write tests that start from given/when/then, use real collaborators
6
6
  # Isolated testing style
7
7
 
8
8
  An opinionated way of writing tests. The examples are TypeScript and vitest, but the rules are about
9
- test design rather than any framework. Each rule exists because of a specific failure it would have
9
+ test design, not about any framework. Each rule exists because of a specific failure it would have
10
10
  caught.
11
11
 
12
12
  ## Start with Given, When, Then
@@ -21,15 +21,15 @@ it("refuses an order once the offer has closed", async () => {
21
21
  });
22
22
  ```
23
23
 
24
- Starting here rather than arriving here does three things.
24
+ Starting at this point, and not drifting to it, does three things.
25
25
 
26
26
  **It lowers the cost of starting.** You only have to say what the situation is, what happens, and
27
27
  what should result. That is a smaller question than "how do I test this?", and you can usually
28
28
  answer it before you can answer the bigger one.
29
29
 
30
30
  **It designs the interface.** Filling in `// When` forces you to name the single action under test,
31
- in the caller's vocabulary rather than the implementation's. A step you cannot write as one
32
- `// When` is usually telling you the interface is wrong, not the test.
31
+ in the caller's vocabulary, not the implementation's. A step you cannot write as one `// When` is
32
+ usually telling you the interface is wrong, not the test.
33
33
 
34
34
  **It keeps the test readable as documentation.** Tests are read far more often than they are
35
35
  written, and without the structure it is easy to produce a body where essential behaviour and
@@ -50,7 +50,7 @@ it("refuses an order once the offer has closed", async () => {
50
50
  });
51
51
  ```
52
52
 
53
- `// Given an offer` restates the code and is worth nothing.
53
+ `// Given an offer` restates the code and adds no information.
54
54
  `// Given an offer that closed while the customer was on the page` says which case this is and why
55
55
  it matters. This holds whether or not the test is written first.
56
56
 
@@ -59,57 +59,57 @@ it matters. This holds whether or not the test is written first.
59
59
  A stub asserts that your code called something. A simulator asserts that it called the service
60
60
  correctly.
61
61
 
62
- That difference is the whole argument. A stub answers whatever you told it to answer, so it agrees
63
- with your understanding of the API by construction. It cannot disagree with you, which means it
64
- cannot find the case where your understanding is wrong. A simulator holds real state and applies the
65
- real rules, so a wrong call fails at the point the real service would have failed.
62
+ That difference is the whole argument. A stub answers whatever you told it to answer. It agrees with
63
+ your understanding of the API by construction. It cannot disagree with you, which means it cannot
64
+ find the case where your understanding is wrong. A simulator holds real state and applies the real
65
+ rules. A wrong call fails at the point the real service would have failed.
66
66
 
67
- The evidence: on a real project, replacing AWS SDK stubs with a simulator immediately caught two
68
- bugs that had already shipped.
67
+ The evidence comes from a real project. Replacing AWS SDK stubs with a simulator immediately caught
68
+ two bugs that had already shipped.
69
69
 
70
70
  - A Secrets Manager secret name ending in a hyphen and six characters. AWS appends exactly that
71
- suffix to a secret ARN, so the name was ambiguous with the ARN form, which AWS advises against.
72
- The stub had no opinion, because a stub has no naming rules.
71
+ suffix to a secret ARN. The name was ambiguous with the ARN form, which AWS advises against. The
72
+ stub had no opinion, because a stub has no naming rules.
73
73
  - A Cognito `SECRET_HASH` computed the wrong way. The stub accepted it, because the stub was never
74
74
  going to check a signature.
75
75
 
76
76
  So the order to reach for things:
77
77
 
78
78
  1. A simulator that holds real state and applies real rules.
79
- 2. The real thing, when it runs in process and needs nothing external.
79
+ 2. The real thing, when it runs in process and needs no external service.
80
80
  3. A stub, only for something with no rules worth modelling, such as a clock or a random source.
81
81
 
82
82
  The first two options need the implementation to be swappable, so give each collaborator a single
83
- point of entry: one place that wires the real service in production and a simulation in tests. The
83
+ point of entry, one place that wires the real service in production and a simulation in tests. The
84
84
  driver pattern is one way to arrange that, but the name matters far less than the swap having one
85
85
  home.
86
86
 
87
- Simulating in process pays off beyond avoiding stubs. There is nothing to deploy before running the
87
+ Simulating in process pays off beyond avoiding stubs. No deployment is needed before running the
88
88
  tests, a debugger steps through the collaborator's state alongside your own, and no state is shared
89
89
  between processes, so several layers can be exercised together and still run in parallel at the
90
90
  speed of a unit test.
91
91
 
92
- Be honest about the limit. An in-memory implementation is not the real service and cannot be relied
93
- on to behave identically. Keep a thin layer of tests against the real thing for the flows where that
94
- matters, and treat any divergence you find as a bug in the simulation rather than a quirk to work
95
- around.
92
+ Be honest about the limit. An in-memory implementation only approximates the real service, and
93
+ cannot be relied on to behave identically. Keep a thin layer of tests against the real thing for the
94
+ flows where that matters, and treat any divergence you find as a bug in the simulation rather than a
95
+ quirk to work around.
96
96
 
97
97
  ## Keep setup cheap and independent
98
98
 
99
- Tangled shared fixtures are not a discipline failure. Teams share setup roughly in proportion to how
100
- expensive it is to build. When getting a test into the right state means threading through a web of
101
- existing fixtures, reusing what is already there is the rational move and each reuse adds another
102
- edge to the graph. That is how a suite arrives at setup that no one dares to touch.
99
+ Tangled shared fixtures come from economics, not from indiscipline. Teams share setup roughly in
100
+ proportion to how expensive it is to build. When getting a test into the right state means threading
101
+ through a web of existing fixtures, reusing what is already there is the rational move, and each
102
+ reuse adds another edge to the graph. That is how a suite arrives at setup that no one dares to
103
+ touch.
103
104
 
104
- The fix is not to share less. Shared factories for test entities are exactly what you want: a
105
+ The fix is to share differently. Shared factories for test entities are exactly what you want. A
105
106
  factory that constructs a type is worth writing once and using everywhere. What has to be avoided is
106
107
  those factories getting tangled up with each other. Each piece of setup should stand on its own.
107
108
 
108
109
  Independence comes from taking dependencies explicitly rather than reaching for ambient state. A
109
110
  factory that is handed what it needs stays pure, and the test decides what to hand it.
110
- `@kensio/part-factory` builds this in: factories take a `dependencies` object as a second argument
111
- at call time, so a factory that needs a simulated AWS is given one rather than going looking for
112
- one.
111
+ `@kensio/part-factory` builds this in. Factories take a `dependencies` object as a second argument
112
+ at call time. A factory that needs a simulated AWS is given one rather than going looking for one.
113
113
 
114
114
  ```typescript
115
115
  // Given an order that exists in this test's own simulated AWS.
@@ -118,8 +118,8 @@ const order = await orderFactory.make({ total: 5000 }, { simAws });
118
118
  ```
119
119
 
120
120
  A factory built that way can be shared as widely as you like and still stand on its own, because
121
- nothing it does depends on what another factory did first. Prefer collaborators and factories that
122
- need only instantiation: no side effects, nothing to undo, nothing to coordinate.
121
+ everything it does is independent of what another factory did first. Prefer collaborators and
122
+ factories that need only instantiation, with no side effects, no cleanup and no coordination.
123
123
 
124
124
  For a step specific to one test rather than a reusable factory, ask whether the step belongs to the
125
125
  test or the test belongs to the step. A helper confined to one file can be pulled back inline later
@@ -128,8 +128,8 @@ It is that reversibility worth preserving, not locality for its own sake.
128
128
 
129
129
  ## Get isolation from the data, not from setup and teardown
130
130
 
131
- Randomised values make collisions impossible, so there is nothing to tear down and no ordering to
132
- depend on. Randomised is enough. Guaranteed uniqueness is not needed, and a UUID has no realistic
131
+ Randomised values make collisions impossible. No teardown is required, and there is no ordering to
132
+ depend on. Randomised is enough. Guaranteed uniqueness is unnecessary, and a UUID has no realistic
133
133
  chance of colliding anyway.
134
134
 
135
135
  Do not do this:
@@ -151,7 +151,7 @@ afterEach(async () => {
151
151
 
152
152
  That test cannot run beside another test using the same name, the `let` is only mutable so that
153
153
  `afterEach` can reach it, and a failure part way through leaves the next test to fail for a reason
154
- that has nothing to do with it.
154
+ unrelated to it.
155
155
 
156
156
  Do this instead:
157
157
 
@@ -168,11 +168,11 @@ it("serves an uploaded object", async () => {
168
168
  The environment those tests run against can be shared, and should be built the way production is
169
169
  built. A realistic environment that several tests read from is closer to production than a minimal
170
170
  one rebuilt per test, and it is faster. The isolation comes from the names and identifiers, so
171
- sharing the environment costs nothing.
171
+ sharing the environment is free.
172
172
 
173
173
  Faker is the source of these values. Prefer a generator that produces a realistic value of the right
174
174
  kind (`faker.internet.email()`, `faker.string.uuid()`, `faker.company.name()`) over a counter or a
175
- literal with a suffix, so the test data also exercises the shapes production data has.
175
+ literal with a suffix. The test data also exercises the shapes production data has.
176
176
 
177
177
  ## Assert behaviour, not call counts
178
178
 
@@ -223,9 +223,9 @@ expect(computeSecretHash(username, clientId, clientSecret)).toBe(
223
223
  Two ways out, in order of preference:
224
224
 
225
225
  1. Let a real implementation validate it. A simulated Cognito checks a `SECRET_HASH` the way Cognito
226
- checks it, so a sign-in that succeeds against the simulation is evidence the hash is right.
227
- 2. Pin against an independent authority: a value from the service's own documentation, a published
228
- test vector, or a value produced by a different implementation.
226
+ checks it. A sign-in that succeeds against the simulation is evidence the hash is right.
227
+ 2. Pin against an independent authority, such as a value from the service's own documentation, a
228
+ published test vector, or a value produced by a different implementation.
229
229
 
230
230
  The same rule covers snapshot tests of anything the code under test formats. A snapshot records what
231
231
  the code does, not what it should do.
@@ -233,8 +233,8 @@ the code does, not what it should do.
233
233
  ## Nothing at the top level but imports
234
234
 
235
235
  In an ideal vitest or jest file, the only things outside the top-level `describe()` are the imports.
236
- State, construction and helpers all live inside it, so the file reads as a description of behaviour
237
- rather than as a program that happens to contain some tests.
236
+ State, construction and helpers all live inside it. The file reads as a description of behaviour and
237
+ not as a program that happens to contain some tests.
238
238
 
239
239
  ```typescript
240
240
  import { describe, expect, it } from "vitest";
@@ -248,18 +248,18 @@ describe("placing an order", () => {
248
248
  });
249
249
  ```
250
250
 
251
- This is mostly a consequence of the other rules rather than an extra one. What usually accumulates
252
- at the top level of a test file is module-level state the tests share, a mutable handle that exists
253
- so `afterEach` can reach it, and hoisted mock registrations and the rules above have already
254
- turned down all three. So a top level that will not stay empty is a useful signal that something
255
- further up has slipped. Imported factories are not a problem here: they arrive as imports precisely
256
- because they stand on their own.
251
+ This is mostly a consequence of the other rules, not an extra one. What usually accumulates at the
252
+ top level of a test file is module-level state the tests share, a mutable handle that exists so
253
+ `afterEach` can reach it, and hoisted mock registrations. The rules above have already turned down
254
+ all three. So a top level that will not stay empty is a useful signal that something further up has
255
+ slipped. Imported factories are fine here. They arrive as imports precisely because they stand on
256
+ their own.
257
257
 
258
258
  ## Put test support beside the code, not in the test file
259
259
 
260
260
  A test file should hold tests. Where support lives depends on what it is for.
261
261
 
262
- A factory for a **type** belongs beside the type it constructs, exported, so that no consumer ever
262
+ A factory for a **type** belongs beside the type it constructs, exported. That no consumer ever
263
263
  hand-rolls the literal:
264
264
 
265
265
  ```
@@ -272,15 +272,15 @@ src/orders/
272
272
  A library that defines a shape other code has to construct should export the factory for it.
273
273
 
274
274
  A step written for **one test** stays in that test's file, inside the `describe`. Promoting it later
275
- when another test wants it is fine make it independent first, so what spreads is a self-contained
276
- factory rather than a dependency on how some other test left things.
275
+ when another test wants it is fine. Make it independent first, so what spreads is a self-contained
276
+ factory (not a dependency on how some other test left things).
277
277
 
278
- If a test file is mostly setup, that is a signal but the fix is usually cheaper construction, not
279
- a shared fixture. Scroll the file and see how much of it is `it(...)` bodies making assertions.
278
+ If a test file is mostly setup, that is a signal. The fix is usually cheaper construction, not a
279
+ shared fixture. Scroll the file and see how much of it is `it(...)` bodies making assertions.
280
280
 
281
281
  ## Tools that help
282
282
 
283
- These serve the style; they are not the style, which holds without them.
283
+ These serve the style. The style holds without them.
284
284
 
285
285
  - [Faker](https://fakerjs.dev/) for randomised, realistic values.
286
286
  - [`@kensio/part-factory`](https://partfactory.dev/) for typed factories that need only