@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -10
- package/package.json +1 -1
- package/skills/isolated-testing-style/SKILL.md +52 -52
|
@@ -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.
|
|
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
|
|
4
|
-
|
|
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
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49
|
-
|
|
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
|
+
"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
|
|
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
|
|
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
|
|
32
|
-
|
|
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
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
72
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
93
|
-
on to behave identically. Keep a thin layer of tests against the real thing for the
|
|
94
|
-
matters, and treat any divergence you find as a bug in the simulation rather than a
|
|
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
|
|
100
|
-
expensive it is to build. When getting a test into the right state means threading
|
|
101
|
-
existing fixtures, reusing what is already there is the rational move
|
|
102
|
-
edge to the graph. That is how a suite arrives at setup that no one dares to
|
|
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
|
|
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
|
|
111
|
-
at call time
|
|
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
|
-
|
|
122
|
-
need only instantiation
|
|
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
|
|
132
|
-
depend on. Randomised is enough. Guaranteed uniqueness is
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
227
|
-
2. Pin against an independent authority
|
|
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
|
|
237
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
|
276
|
-
factory
|
|
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
|
|
279
|
-
|
|
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
|
|
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
|