rhachet-roles-ehmpathy 1.8.0 → 1.9.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/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].v3.md +87 -0
- package/dist/logic/roles/mechanic/.briefs/architecture/directional-dependencies.md +49 -40
- package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.knowledge.externalized.md +17 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/bad-practice/forbid.positional-args.md +43 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/best-practice/require.namedargs.md +6 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/.readme.md +0 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/best-practice/declastruct.[demo].md +485 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.nullable.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.undefined.md +15 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.refs.immuatble.md +9 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/best-practice/ref.package.domain-objects.[readme].md +585 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.operations/best-practice/require.sync.names.md +14 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/bad-practices/forbid.hide_errors.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.[demo].shell.md +17 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.md +28 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/directional-dependencies.md +82 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/dot-test-and-dot-temp.md +20 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.typescript.utils/best-practice/ref.package.as-command.[tips].md +7 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.acceptance/best-practice/blackbox.md +5 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.run.[lesson].md +18 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.use.[lesson].md +20 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].md +3 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_integ.md +8 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_units.md +9 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md +280 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/prefer.datadriven.md +41 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/ref.test-fns.[readme].md +185 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/whento.snapshots.[lesson].md +23 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/.readme.md +1 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=practices.terms=forbid_prefer_desire_require.md +13 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=software.terms=prodcode_vs_testcode.md +7 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/.readme.md +3 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.chill-nature.md +0 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.lowercase.md +0 -0
- package/dist/logic/roles/mechanic/getMechanicRole.js +1 -1
- package/dist/logic/roles/mechanic/getMechanicRole.js.map +1 -1
- package/package.json +2 -2
- package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].[idea].md +0 -35
- package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].md +0 -20
- /package/dist/logic/roles/architect/.briefs/{criteria.practices → practices}/prefer.env_access.prep_over_dev.md +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
.tactic = arch:directional-deps
|
|
2
|
+
|
|
3
|
+
.what
|
|
4
|
+
enforce strict top-down dependency flow across layered system boundaries — lower layers must never import from higher ones
|
|
5
|
+
|
|
6
|
+
.scope
|
|
7
|
+
- applies to all folders and modules within `src/`
|
|
8
|
+
- required for `contract/`, `access/`, `domain.objects/`, `domain.operations/`
|
|
9
|
+
- governs imports, module references, and stitched flow boundaries
|
|
10
|
+
|
|
11
|
+
.why
|
|
12
|
+
- upholds **separation of concerns** and enforces **clean architecture**
|
|
13
|
+
- prevents circular dependencies and tangled system boundaries
|
|
14
|
+
- makes each layer easier to test, replace, and understand in isolation
|
|
15
|
+
- aligns with `arch:bounded-contexts` and enables predictable top-down orchestration
|
|
16
|
+
|
|
17
|
+
.structure
|
|
18
|
+
\`\`\`
|
|
19
|
+
src/
|
|
20
|
+
contract/ // topmost — public interfaces, local commands
|
|
21
|
+
api/ // public invocable api endpoints, deployed and exposed by the project; e.g., via `aws-lambda`
|
|
22
|
+
cmd/ // private internal use entrypoints, supported by the project; e.g., via `as-command`
|
|
23
|
+
sdk/ // public software development kit exports, supported by the project; e.g., `export ...`
|
|
24
|
+
cli/ // public command line interface contracts, supported by the project; e.g., via `commander`
|
|
25
|
+
|
|
26
|
+
access/ // infrastructure layer (daos, sdks, svcs)
|
|
27
|
+
daos/ // private persistence logic — may reference domain objects
|
|
28
|
+
sdks/ // remote third party contracts, from any alt org — may declare their own domain.objects
|
|
29
|
+
svcs/ // remote first party contracts, from our own org — may declare their own domain.objects
|
|
30
|
+
|
|
31
|
+
domain.objects/ // canonical domain declarations
|
|
32
|
+
domain.operations/ // domain behavior + business rules
|
|
33
|
+
|
|
34
|
+
infra/ // infrastructure specific adapters
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
.how
|
|
38
|
+
- each layer may depend **only on the layers below it**
|
|
39
|
+
- `contract/` may depend on `domain.objects/` and `domain.operations/`
|
|
40
|
+
- `access/` may depend on `domain.objects/` and `domain.operations/`
|
|
41
|
+
- `domain.operations/` may depend on `domain.objects/` or `infra/`
|
|
42
|
+
- `domain.objects/` must not depend on anything outside its own layer
|
|
43
|
+
- `infra/` must not depend on anything outside its own layer
|
|
44
|
+
|
|
45
|
+
- stitched flows live in `domain.operations/` or `contract/commands/` and orchestrate downstream only
|
|
46
|
+
- never import upward across layers (e.g., `domain.objects/` importing `access/`)
|
|
47
|
+
- shared types must follow the same directional rules
|
|
48
|
+
|
|
49
|
+
.enforcement
|
|
50
|
+
- imports that violate top-down boundary = **BLOCKER**
|
|
51
|
+
- circular dependencies between layers = **BLOCKER**
|
|
52
|
+
- logic in `domain.objects/` must never reach into `access/`, `contract/`, or `domain.operations/`
|
|
53
|
+
- logic in `domain.operations/` must not reference infrastructure concerns; they can can only leverage `infra/` adapters
|
|
54
|
+
- logic in `access/` may use domain layers but must remain free of domain knowledge and business rules
|
|
55
|
+
- logic in `infra/` must remain free of domain knowledge and business rules
|
|
56
|
+
|
|
57
|
+
.examples
|
|
58
|
+
|
|
59
|
+
✅ positive
|
|
60
|
+
\`\`\`ts
|
|
61
|
+
// contract/endpoints/sendInvoice.ts
|
|
62
|
+
import { generateInvoice } from '@/domain.operations/generateInvoice';
|
|
63
|
+
import { invoiceDao } from '@/access/daos/invoiceDao';
|
|
64
|
+
|
|
65
|
+
// access/daos/jobDao.ts
|
|
66
|
+
import { Job } from '@/domain.objects/Job';
|
|
67
|
+
|
|
68
|
+
// domain.operations/calculateTotal.ts
|
|
69
|
+
import { LineItem } from '@/domain.objects/LineItem';
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
❌ negative
|
|
73
|
+
\`\`\`ts
|
|
74
|
+
// domain.objects/Customer.ts
|
|
75
|
+
import { customerDao } from '@/access/daos/customerDao'; // ⛔ illegal upward import
|
|
76
|
+
|
|
77
|
+
// domain.operations/InvoiceOps.ts
|
|
78
|
+
import { runFlow } from '@/contract/commands'; // ⛔ direction violation
|
|
79
|
+
|
|
80
|
+
// access/svcs/sdkWrapper.ts
|
|
81
|
+
import { dispatchFlow } from '@/contract/'; // ⛔ bottom-up reference
|
|
82
|
+
\`\`\`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
prefer
|
|
2
|
+
|
|
3
|
+
.temp/ directories over
|
|
4
|
+
- tmp/
|
|
5
|
+
- temp
|
|
6
|
+
|
|
7
|
+
.test/ directories over
|
|
8
|
+
- __test__
|
|
9
|
+
- __fixtures__
|
|
10
|
+
- __test_assets__
|
|
11
|
+
- __test_utils__
|
|
12
|
+
- etc
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
its easier on the eyes and more consistent with broader patterns
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
this is a NITPICK level violation
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
1. lookup the organization to use from declapract.use.yml
|
|
2
|
+
2. declare the AWS_PROFILE based on the pattern `AWS_PROFILE=$organization.dev`
|
|
3
|
+
1. always use .dev, never .prod
|
|
4
|
+
2. this specifies which remote resources we'll have access to for the session
|
|
5
|
+
3. we always test against dev resources, to avoid prod pollution
|
|
6
|
+
3. run npm run start:testdb (or provision:integration-test-db, if start:testdb is not available yet)
|
|
7
|
+
1. this setsup the local testdb against which the tests can be run
|
|
8
|
+
|
|
9
|
+
then, you can run the tests you need to
|
|
10
|
+
|
|
11
|
+
e.g.,
|
|
12
|
+
|
|
13
|
+
AWS_PROFILE=$organization.dev npm run test:integration -- syncPhoneFromWhodis.integration.test.ts
|
|
14
|
+
AWS_PROFILE=$organization.dev npm run test:unit -- syncPhoneFromWhodis.test.ts
|
|
15
|
+
|
|
16
|
+
etc
|
|
17
|
+
|
|
18
|
+
check package json for the other test variants you can run
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
instead of rerunning tests over and over and getting the `head` to check what happened while preserving context
|
|
2
|
+
|
|
3
|
+
its BEST
|
|
4
|
+
|
|
5
|
+
to `| tee` into a `@gitroot/.log/test/(unit|integration|acceptance)/run.$ISOTIMESTAMP.out` file
|
|
6
|
+
|
|
7
|
+
then you can review that file over and over
|
|
8
|
+
|
|
9
|
+
also, other agents can review as well in parallel
|
|
10
|
+
|
|
11
|
+
last, it can also be used to compare progress in changes of tests
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
so, pretty much ALWAYS, you should ` | tee` into one of these files
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
===
|
|
19
|
+
|
|
20
|
+
best practice is to run test via `.skills/run.test.sh`, which does this for you
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
to create an integration test, just add a file with .integration.test.ts extension (collocated) and dont mock anything
|
|
3
|
+
|
|
4
|
+
you can spy, but never mock
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
use the less on in src/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md for preference on pattern
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
for unit tests:
|
|
3
|
+
|
|
4
|
+
only test that the behaviors explicitly important for that test
|
|
5
|
+
|
|
6
|
+
no need to overtest, only whatever is scoped for that test and unique to it
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
use the less on in src/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md for preference on pattern
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# How to Write BDD Style Tests
|
|
2
|
+
|
|
3
|
+
This guide explains the pattern for writing integration tests using `test-fns` with `given`, `when`, `then`, and `useBeforeAll`.
|
|
4
|
+
|
|
5
|
+
## Core Pattern
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { given, when, then, useBeforeAll } from 'test-fns';
|
|
9
|
+
|
|
10
|
+
describe('featureName', () => {
|
|
11
|
+
// Shared setup for all tests in the describe block
|
|
12
|
+
const dbConnection = useBeforeAll(() => getDatabaseConnection());
|
|
13
|
+
afterAll(async () => dbConnection.end());
|
|
14
|
+
|
|
15
|
+
given('[case1] description of the initial state', () => {
|
|
16
|
+
// Setup specific to this case, shared across all when/then blocks
|
|
17
|
+
const scene = useBeforeAll(async () => {
|
|
18
|
+
// Create test data
|
|
19
|
+
const entity = await createEntity({ dbConnection });
|
|
20
|
+
return { entity };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
when('[t0] action or event occurs', () => {
|
|
24
|
+
then('expected outcome', async () => {
|
|
25
|
+
// Execute and verify using scene.entity
|
|
26
|
+
const result = await performAction({ id: scene.entity.id });
|
|
27
|
+
expect(result).toEqual(expectedValue);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
when('[t1] different action occurs', () => {
|
|
32
|
+
then('different expected outcome', async () => {
|
|
33
|
+
// Another test using the same scene
|
|
34
|
+
const result = await performOtherAction({ id: scene.entity.id });
|
|
35
|
+
expect(result).toEqual(otherExpectedValue);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Key Principles
|
|
43
|
+
|
|
44
|
+
### 1. Wrap Everything in `describe`
|
|
45
|
+
|
|
46
|
+
All tests for a feature should be wrapped in a single `describe` block:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
describe('syncPhoneFromWhodis', () => {
|
|
50
|
+
// all given/when/then blocks go here
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Use `useBeforeAll` for shared resources
|
|
55
|
+
|
|
56
|
+
Instead of `let` + `beforeAll` + `afterAll`:
|
|
57
|
+
|
|
58
|
+
e.g.,
|
|
59
|
+
```typescript
|
|
60
|
+
// ❌ Don't do this
|
|
61
|
+
let dbConnection: DatabaseConnection;
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
dbConnection = await getDatabaseConnection();
|
|
64
|
+
});
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await dbConnection.end();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ✅ Do this
|
|
70
|
+
const dbConnection = useBeforeAll(() => getDatabaseConnection());
|
|
71
|
+
afterAll(async () => dbConnection.end());
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Label given(scenes) with `[caseN]`
|
|
75
|
+
|
|
76
|
+
Each `given` block should have a unique case label:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
given('[case1] doer with outdated phone', () => { ... });
|
|
80
|
+
given('[case2] doer with matching phone', () => { ... });
|
|
81
|
+
given('[case3] doer does not exist', () => { ... });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Label when(event) with `[tN]`
|
|
85
|
+
|
|
86
|
+
Each `when` block should have an event time index label. The counter resets within each `given` block:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
given('[case1] first scenario', () => {
|
|
90
|
+
when('[t0] command executed in PLAN mode', () => { ... });
|
|
91
|
+
when('[t1] command executed in EXECUTE mode', () => { ... });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
given('[case2] second scenario', () => {
|
|
95
|
+
when('[t0] first action', () => { ... }); // counter resets to 0
|
|
96
|
+
when('[t1] second action', () => { ... });
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. One Behavioral Assertion per `then` Block
|
|
101
|
+
|
|
102
|
+
Each `then` block should test a single behavioral assertion. This makes test failures more precise and test names more descriptive:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// ❌ Don't do this - multiple assertions in one then
|
|
106
|
+
when('[t0] command executed in PLAN mode', () => {
|
|
107
|
+
then('decision is UPDATE and doer remains unchanged', async () => {
|
|
108
|
+
const result = await command({ mode: 'PLAN' });
|
|
109
|
+
expect(result.decision).toEqual('UPDATE');
|
|
110
|
+
expect(result.before.doer.contactPhoneNumber).toEqual('+13175550200');
|
|
111
|
+
|
|
112
|
+
const doerAfter = await doerDao.findByUnique({ dbConnection, userUuid });
|
|
113
|
+
expect(doerAfter?.contactPhoneNumber).toEqual('+13175550200');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ✅ Do this - separate then blocks for each behavioral assertion
|
|
118
|
+
when('[t0] command executed in PLAN mode', () => {
|
|
119
|
+
then('decision is "UPDATE"', async () => {
|
|
120
|
+
const result = await command({ mode: 'PLAN' });
|
|
121
|
+
expect(result.decision).toEqual('UPDATE');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
then('before.doer.contactPhoneNumber is "+13175550200"', async () => {
|
|
125
|
+
const result = await command({ mode: 'PLAN' });
|
|
126
|
+
expect(result.before.doer.contactPhoneNumber).toEqual('+13175550200');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
then('doer contactPhoneNumber remains unchanged', async () => {
|
|
130
|
+
await command({ mode: 'PLAN' });
|
|
131
|
+
const doerAfter = await doerDao.findByUnique({ dbConnection, userUuid });
|
|
132
|
+
expect(doerAfter?.contactPhoneNumber).toEqual('+13175550200');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 6. Use `scene` for Shared Test Data
|
|
138
|
+
|
|
139
|
+
When multiple `when/then` blocks need the same test data, use `useBeforeAll` to create a `scene`:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
given('[case1] description', () => {
|
|
143
|
+
const scene = useBeforeAll(async () => {
|
|
144
|
+
const doer = await createDoer({ dbConnection });
|
|
145
|
+
const provider = await createProvider({ dbConnection, doerId: doer.id });
|
|
146
|
+
return { doer, provider };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
when('[t0] first test', () => {
|
|
150
|
+
then('outcome', async () => {
|
|
151
|
+
// Access scene.doer and scene.provider
|
|
152
|
+
const result = await action({ doerId: scene.doer.id });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
when('[t1] second test', () => {
|
|
157
|
+
then('outcome', async () => {
|
|
158
|
+
// Same scene is reused
|
|
159
|
+
const result = await otherAction({ providerId: scene.provider.id });
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 7. Cases Without Setup
|
|
166
|
+
|
|
167
|
+
If a case doesn't need setup (e.g., testing error handling with invalid input), skip the `scene`:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
given('[case4] valid userUuid', () => {
|
|
171
|
+
when('[t7] Whodis user cannot be found', () => {
|
|
172
|
+
then('command throws error', async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
command({ userUuid: '00000000-0000-0000-0000-000000000001' }),
|
|
175
|
+
).rejects.toThrow('Whodis user not found');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Complete Example
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { given, when, then, useBeforeAll } from 'test-fns';
|
|
185
|
+
import { getDatabaseConnection } from '../../utils/database/getDatabaseConnection';
|
|
186
|
+
import { myCommand } from './myCommand';
|
|
187
|
+
|
|
188
|
+
describe('myCommand', () => {
|
|
189
|
+
const dbConnection = useBeforeAll(() => getDatabaseConnection());
|
|
190
|
+
afterAll(async () => dbConnection.end());
|
|
191
|
+
|
|
192
|
+
given('[case1] entity exists with state A', () => {
|
|
193
|
+
const scene = useBeforeAll(async () => {
|
|
194
|
+
const entity = await createEntity({
|
|
195
|
+
dbConnection,
|
|
196
|
+
state: 'A',
|
|
197
|
+
});
|
|
198
|
+
return { entity };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
when('[t0] command executed in PLAN mode', () => {
|
|
202
|
+
then('decision is "UPDATE"', async () => {
|
|
203
|
+
const result = await myCommand({
|
|
204
|
+
entityId: scene.entity.id,
|
|
205
|
+
mode: 'PLAN',
|
|
206
|
+
});
|
|
207
|
+
expect(result.decision).toEqual('UPDATE');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
then('entity state remains "A"', async () => {
|
|
211
|
+
await myCommand({ entityId: scene.entity.id, mode: 'PLAN' });
|
|
212
|
+
const entityAfter = await findEntity({ dbConnection, id: scene.entity.id });
|
|
213
|
+
expect(entityAfter.state).toEqual('A');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
when('[t1] command executed in EXECUTE mode', () => {
|
|
218
|
+
then('decision is "UPDATE"', async () => {
|
|
219
|
+
const result = await myCommand({
|
|
220
|
+
entityId: scene.entity.id,
|
|
221
|
+
mode: 'EXECUTE',
|
|
222
|
+
});
|
|
223
|
+
expect(result.decision).toEqual('UPDATE');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
then('after.state is "B"', async () => {
|
|
227
|
+
const result = await myCommand({
|
|
228
|
+
entityId: scene.entity.id,
|
|
229
|
+
mode: 'EXECUTE',
|
|
230
|
+
});
|
|
231
|
+
expect(result.after.state).toEqual('B');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
then('entity state is updated to "B"', async () => {
|
|
235
|
+
await myCommand({ entityId: scene.entity.id, mode: 'EXECUTE' });
|
|
236
|
+
const entityAfter = await findEntity({ dbConnection, id: scene.entity.id });
|
|
237
|
+
expect(entityAfter.state).toEqual('B');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
given('[case2] entity already in state B', () => {
|
|
243
|
+
const scene = useBeforeAll(async () => {
|
|
244
|
+
const entity = await createEntity({
|
|
245
|
+
dbConnection,
|
|
246
|
+
state: 'B',
|
|
247
|
+
});
|
|
248
|
+
return { entity };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
when('[t0] command executed', () => {
|
|
252
|
+
then('decision is "NOCHANGE"', async () => {
|
|
253
|
+
const result = await myCommand({
|
|
254
|
+
entityId: scene.entity.id,
|
|
255
|
+
mode: 'EXECUTE',
|
|
256
|
+
});
|
|
257
|
+
expect(result.decision).toEqual('NOCHANGE');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
given('[case3] invalid entityId', () => {
|
|
263
|
+
when('[t0] command executed', () => {
|
|
264
|
+
then('throws error', async () => {
|
|
265
|
+
await expect(
|
|
266
|
+
myCommand({ entityId: 'invalid-id', mode: 'PLAN' }),
|
|
267
|
+
).rejects.toThrow('Entity not found');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Benefits
|
|
275
|
+
|
|
276
|
+
1. **Readable test output**: Test names clearly show the scenario being tested
|
|
277
|
+
2. **Efficient setup**: `useBeforeAll` runs once per `given` block, not per test
|
|
278
|
+
3. **Immutable references**: `const scene` and `const dbConnection` prevent accidental reassignment
|
|
279
|
+
4. **Clear labeling**: `[caseN]` and `[tN]` labels make it easy to identify and discuss specific tests
|
|
280
|
+
5. **Black-box testing**: Tests interact only through the contract layer, not internal implementations
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
when possible, prefer data driven, caselist based, tests
|
|
2
|
+
|
|
3
|
+
this is especially applicable for unit tests, which often evaluate a transform
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
for example
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
|
|
13
|
+
const TEST_CASES = [
|
|
14
|
+
{
|
|
15
|
+
description: 'capitalizes the first word in a sentence',
|
|
16
|
+
given: {
|
|
17
|
+
input: 'the bird is in the basket',
|
|
18
|
+
},
|
|
19
|
+
expect: {
|
|
20
|
+
output: 'The bird is in the basket',
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: 'retains existing capitals in the sentence',
|
|
25
|
+
given: {
|
|
26
|
+
input: 'that Doctor Goose is a loon!',
|
|
27
|
+
},
|
|
28
|
+
expect: {
|
|
29
|
+
output: 'That Doctor Goose is a loon!',
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
describe('asSentenceCase', () => {
|
|
35
|
+
TEST_CASES.map(thisCase => test(thisCase.description, () => {
|
|
36
|
+
const output = asSentenceCase(thisCase.given.input);
|
|
37
|
+
expect(output).toEqual(thisCase.expect.output);
|
|
38
|
+
}))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
```
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# test-fns
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
write usecase driven tests systematically for simpler, safer, and more readable code
|
|
7
|
+
|
|
8
|
+
# purpose
|
|
9
|
+
|
|
10
|
+
establishes a pattern of writing tests for simpler, safer, and more readable code.
|
|
11
|
+
|
|
12
|
+
by defining tests in terms of usecases (`given`, `when`, `then`) your tests are
|
|
13
|
+
- simpler to write
|
|
14
|
+
- easier to read
|
|
15
|
+
- safer to trust
|
|
16
|
+
|
|
17
|
+
# install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install --save test-fns
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
# use
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
type Plant = { id: number, hydration: 'DRY' | 'WET' };
|
|
27
|
+
const doesPlantNeedWater = (plant: Plant) => plant.hydration === 'DRY';
|
|
28
|
+
|
|
29
|
+
describe('doesPlantNeedWater', () => {
|
|
30
|
+
given('a plant', () => {
|
|
31
|
+
when('the plant doesnt have enough water', () => {
|
|
32
|
+
const plant: Plant = {
|
|
33
|
+
id: 7,
|
|
34
|
+
hydration: 'DRY',
|
|
35
|
+
};
|
|
36
|
+
then('it should return true', () => {
|
|
37
|
+
expect(doesPlantNeedWater(plant)).toEqual(true)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
produces
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
PASS src/givenWhenThen.test.ts
|
|
48
|
+
doesPlantNeedWater
|
|
49
|
+
given: a plant
|
|
50
|
+
when: the plant doesnt have enough water
|
|
51
|
+
✓ then: it should return true (1 ms)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
# features
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### .runIf(condition) && .skipIf(condition)
|
|
58
|
+
|
|
59
|
+
skip running the suite if the condition is not met
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
describe('your test', () => {
|
|
63
|
+
given.runIf(onLocalMachine)('some test that should only run locally', () => {
|
|
64
|
+
then.skipIf(onProduction)('some test that should not run against production', () => {
|
|
65
|
+
expect(onProduction).toBeFalse()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### usePrep
|
|
72
|
+
|
|
73
|
+
prepare test scenarios within a .given/.when block asynchronously, without any `let`s or `beforeAll`s
|
|
74
|
+
|
|
75
|
+
`usePrep` accepts a `mode` option to control when setup runs:
|
|
76
|
+
- `mode: 'beforeAll'` - runs setup once for all tests (default)
|
|
77
|
+
- `mode: 'beforeEach'` - runs setup fresh before each test
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
given('an overdue invoice', () => {
|
|
81
|
+
const invoice = usePrep(async () => {
|
|
82
|
+
const invoiceOverdue = await ... // your logic
|
|
83
|
+
return invoiceOverdue;
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
then('it should invoke a reminder', async () => {
|
|
87
|
+
const result = await nurtureInvoice({ invoice }, context)
|
|
88
|
+
expect(result.sent.reminder).toEqual(true)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**useBeforeAll and useBeforeEach are convenience wrappers** around `usePrep`:
|
|
94
|
+
- `useBeforeAll(setup)` is equivalent to `usePrep(setup, { mode: 'beforeAll' })`
|
|
95
|
+
- `useBeforeEach(setup)` is equivalent to `usePrep(setup, { mode: 'beforeEach' })`
|
|
96
|
+
|
|
97
|
+
Use the named functions for clarity about when setup runs.
|
|
98
|
+
|
|
99
|
+
### useBeforeAll
|
|
100
|
+
|
|
101
|
+
prepare test resources once for all tests in a suite, optimizing setup time for expensive operations
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
describe('spaceship refueling system', () => {
|
|
105
|
+
given('a spaceship that needs to refuel', () => {
|
|
106
|
+
const spaceship = useBeforeAll(async () => {
|
|
107
|
+
// This runs once before all tests in this suite
|
|
108
|
+
const ship = await prepareExampleSpaceship();
|
|
109
|
+
await ship.dock();
|
|
110
|
+
return ship;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
when('no changes are made', () => {
|
|
114
|
+
then('it should be docked', async () => {
|
|
115
|
+
expect(spaceship.isDocked).toEqual(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
then('it should need fuel', async () => {
|
|
119
|
+
expect(spaceship.fuelLevel).toBeLessThan(spaceship.fuelCapacity);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
when('it connects to the fuel station', () => {
|
|
124
|
+
const result = useBeforeAll(async () => await spaceship.connectToFuelStation());
|
|
125
|
+
|
|
126
|
+
then('it should be connected', async () => {
|
|
127
|
+
expect(result.connected).toEqual(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
then('it should calculate required fuel', async () => {
|
|
131
|
+
expect(result.fuelNeeded).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### useBeforeEach
|
|
139
|
+
|
|
140
|
+
prepare fresh test resources before each test, ensuring test isolation
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
describe('spaceship combat system', () => {
|
|
144
|
+
given('a spaceship in battle', () => {
|
|
145
|
+
// This runs before each test, ensuring a fresh spaceship
|
|
146
|
+
const spaceship = useBeforeEach(async () => {
|
|
147
|
+
const ship = await prepareExampleSpaceship();
|
|
148
|
+
await ship.resetShields();
|
|
149
|
+
return ship;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
when('no changes are made', () => {
|
|
153
|
+
then('it should have full shields', async () => {
|
|
154
|
+
expect(spaceship.shields).toEqual(100);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
then('it should be ready for combat', async () => {
|
|
158
|
+
expect(spaceship.status).toEqual('READY');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
when('it takes damage', () => {
|
|
163
|
+
const result = useBeforeEach(async () => await spaceship.takeDamage(25));
|
|
164
|
+
|
|
165
|
+
then('it should reduce shield strength', async () => {
|
|
166
|
+
expect(spaceship.shields).toEqual(75);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
then('it should return damage report', async () => {
|
|
170
|
+
expect(result.damageReceived).toEqual(25);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**When to use each:**
|
|
178
|
+
- `useBeforeAll`: Use when setup is expensive (database connections, API calls) and tests don't modify the resource
|
|
179
|
+
- `useBeforeEach`: Use when tests modify the resource and need isolation between runs
|
|
180
|
+
- `usePrep`: The base function that powers both - use when you want explicit control over the mode or need to dynamically choose between `beforeAll` and `beforeEach`
|
|
181
|
+
|
|
182
|
+
**Key differences:**
|
|
183
|
+
- All three functions (`usePrep`, `useBeforeAll`, `useBeforeEach`) create a proxy that defers setup until the test framework's lifecycle hooks run
|
|
184
|
+
- `useBeforeAll` and `useBeforeEach` are just clearer, more readable shortcuts for `usePrep` with a specific mode
|
|
185
|
+
- Choose based on readability: use `useBeforeAll`/`useBeforeEach` for explicit intent, or `usePrep` when mode needs to be configurable
|