@grimoire-cc/cli 0.13.3 → 0.15.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/bin.js +15 -5
- package/dist/bin.js.map +1 -1
- package/dist/commands/agent-paths.d.ts +11 -0
- package/dist/commands/agent-paths.d.ts.map +1 -0
- package/dist/commands/agent-paths.js +69 -0
- package/dist/commands/agent-paths.js.map +1 -0
- package/dist/commands/agent-skills.d.ts +10 -0
- package/dist/commands/agent-skills.d.ts.map +1 -0
- package/dist/commands/agent-skills.js +159 -0
- package/dist/commands/agent-skills.js.map +1 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +62 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +237 -75
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/update.d.ts +1 -2
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +18 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/enforce.d.ts +9 -9
- package/dist/enforce.d.ts.map +1 -1
- package/dist/enforce.js +56 -23
- package/dist/enforce.js.map +1 -1
- package/dist/frontmatter.d.ts +16 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +74 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/grimoire-config.d.ts +6 -0
- package/dist/grimoire-config.d.ts.map +1 -0
- package/dist/grimoire-config.js +23 -0
- package/dist/grimoire-config.js.map +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +13 -8
- package/dist/prompt.js.map +1 -1
- package/dist/remove.d.ts +4 -0
- package/dist/remove.d.ts.map +1 -1
- package/dist/remove.js +8 -0
- package/dist/remove.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +12 -5
- package/dist/resolve.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +45 -2
- package/dist/setup.js.map +1 -1
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +9 -0
- package/dist/summary.js.map +1 -1
- package/package.json +1 -1
- package/packs/dev-pack/agents/grimoire.tdd-specialist.md +194 -27
- package/packs/dev-pack/grimoire.json +0 -38
- package/packs/dev-pack/skills/grimoire.conventional-commit/SKILL.md +69 -65
- package/packs/dotnet-pack/agents/grimoire.csharp-coder.md +110 -113
- package/packs/dotnet-pack/grimoire.json +23 -5
- package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/SKILL.md +252 -0
- package/packs/{dev-pack/skills/grimoire.tdd-specialist → dotnet-pack/skills/grimoire.unit-testing-dotnet}/reference/anti-patterns.md +78 -0
- package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/reference/tdd-workflow-patterns.md +259 -0
- package/packs/frontend-pack/agents/grimoire.angular-coder.md +193 -0
- package/packs/frontend-pack/grimoire.json +7 -0
- package/packs/go-pack/grimoire.json +19 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/SKILL.md +256 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/reference/anti-patterns.md +244 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/reference/tdd-workflow-patterns.md +259 -0
- package/packs/python-pack/grimoire.json +19 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/SKILL.md +239 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/reference/anti-patterns.md +244 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/reference/tdd-workflow-patterns.md +259 -0
- package/packs/rust-pack/grimoire.json +29 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/SKILL.md +243 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/anti-patterns.md +244 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/tdd-workflow-patterns.md +259 -0
- package/packs/ts-pack/agents/grimoire.typescript-coder.md +36 -1
- package/packs/ts-pack/grimoire.json +27 -1
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/SKILL.md +255 -0
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/anti-patterns.md +244 -0
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/tdd-workflow-patterns.md +259 -0
- package/dist/commands/enforce-agent.d.ts +0 -5
- package/dist/commands/enforce-agent.d.ts.map +0 -1
- package/dist/commands/enforce-agent.js +0 -94
- package/dist/commands/enforce-agent.js.map +0 -1
- package/packs/dev-pack/skills/grimoire.tdd-specialist/SKILL.md +0 -248
- package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/language-frameworks.md +0 -388
- package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/tdd-workflow-patterns.md +0 -135
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/SKILL.md +0 -293
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/anti-patterns.md +0 -329
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/framework-guidelines.md +0 -361
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/parameterized-testing.md +0 -378
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-organization.md +0 -476
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-performance.md +0 -576
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/tunit-template.md +0 -438
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/xunit-template.md +0 -303
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grimoire.unit-testing-go
|
|
3
|
+
description: "Go unit testing specialist. Patterns and best practices for the testing stdlib, testify, and gomock. Use when writing tests for .go files, table-driven tests, or asking about Go testing patterns, test helpers, mocking interfaces, benchmarks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Go Unit Testing
|
|
7
|
+
|
|
8
|
+
Focused guidance for writing clean, idiomatic unit tests in Go projects.
|
|
9
|
+
|
|
10
|
+
## Framework Selection
|
|
11
|
+
|
|
12
|
+
### Detection
|
|
13
|
+
|
|
14
|
+
1. Check existing test files first — always match what the project uses
|
|
15
|
+
2. Check `go.mod` for `testify`, `gomock`, `mockery` dependencies
|
|
16
|
+
3. Check existing `*_test.go` files for assertion style
|
|
17
|
+
|
|
18
|
+
### Decision Table
|
|
19
|
+
|
|
20
|
+
| Condition | Use | Reason |
|
|
21
|
+
|-----------|-----|--------|
|
|
22
|
+
| Project has existing tests | **Match existing** | Consistency is paramount |
|
|
23
|
+
| New project, standard needs | **testing** (stdlib) | Built-in, zero dependencies |
|
|
24
|
+
| Need rich assertions + mocking | **testify** | `assert`, `require`, `mock` packages |
|
|
25
|
+
| Strict interface mocking | **gomock** | Code generation, strict expectations |
|
|
26
|
+
| User explicitly requests | **Requested** | Respect user preference |
|
|
27
|
+
|
|
28
|
+
## Naming Conventions
|
|
29
|
+
|
|
30
|
+
Use `TestMethod_Scenario_Expected` with PascalCase/underscores:
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
// Pattern: TestMethod_Scenario_Expected
|
|
34
|
+
func TestGetUser_WithInvalidID_ReturnsNotFound(t *testing.T) { ... }
|
|
35
|
+
func TestCalculateTotal_WithDiscount_AppliesPercentage(t *testing.T) { ... }
|
|
36
|
+
func TestParseConfig_WithMissingFields_ReturnsError(t *testing.T) { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Patterns
|
|
40
|
+
|
|
41
|
+
### AAA with testify
|
|
42
|
+
|
|
43
|
+
```go
|
|
44
|
+
func TestProcessOrder_WithValidOrder_ReturnsID(t *testing.T) {
|
|
45
|
+
// Arrange
|
|
46
|
+
repo := new(MockOrderRepository)
|
|
47
|
+
repo.On("Save", mock.Anything).Return(&Order{ID: "123"}, nil)
|
|
48
|
+
service := NewOrderService(repo)
|
|
49
|
+
|
|
50
|
+
// Act
|
|
51
|
+
result, err := service.ProcessOrder(context.Background(), validOrder)
|
|
52
|
+
|
|
53
|
+
// Assert
|
|
54
|
+
require.NoError(t, err)
|
|
55
|
+
assert.Equal(t, "123", result.ID)
|
|
56
|
+
repo.AssertExpectations(t)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func TestProcessOrder_WithInvalidOrder_ReturnsError(t *testing.T) {
|
|
60
|
+
// Arrange
|
|
61
|
+
service := NewOrderService(nil)
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
_, err := service.ProcessOrder(context.Background(), invalidOrder)
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
assert.ErrorIs(t, err, ErrValidation)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Table-Driven Tests (Go idiom)
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
func TestApplyDiscount(t *testing.T) {
|
|
75
|
+
tests := []struct {
|
|
76
|
+
name string
|
|
77
|
+
price float64
|
|
78
|
+
discount int
|
|
79
|
+
expected float64
|
|
80
|
+
}{
|
|
81
|
+
{"no discount", 100.0, 0, 100.0},
|
|
82
|
+
{"10 percent", 100.0, 10, 90.0},
|
|
83
|
+
{"50 percent", 100.0, 50, 50.0},
|
|
84
|
+
}
|
|
85
|
+
for _, tt := range tests {
|
|
86
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
87
|
+
result := ApplyDiscount(tt.price, tt.discount)
|
|
88
|
+
assert.Equal(t, tt.expected, result)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Table-Driven with Error Cases
|
|
95
|
+
|
|
96
|
+
```go
|
|
97
|
+
func TestValidateOrder(t *testing.T) {
|
|
98
|
+
tests := []struct {
|
|
99
|
+
name string
|
|
100
|
+
order Order
|
|
101
|
+
wantErr error
|
|
102
|
+
}{
|
|
103
|
+
{"valid order", validOrder(), nil},
|
|
104
|
+
{"empty items", Order{Items: nil}, ErrEmptyItems},
|
|
105
|
+
{"negative total", Order{Total: -1}, ErrNegativeTotal},
|
|
106
|
+
}
|
|
107
|
+
for _, tt := range tests {
|
|
108
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
109
|
+
err := ValidateOrder(tt.order)
|
|
110
|
+
if tt.wantErr != nil {
|
|
111
|
+
assert.ErrorIs(t, err, tt.wantErr)
|
|
112
|
+
} else {
|
|
113
|
+
assert.NoError(t, err)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Test Helpers
|
|
121
|
+
|
|
122
|
+
```go
|
|
123
|
+
// t.Helper() marks function as a test helper — errors report caller's line
|
|
124
|
+
func newTestService(t *testing.T, opts ...func(*ServiceConfig)) *OrderService {
|
|
125
|
+
t.Helper()
|
|
126
|
+
cfg := defaultTestConfig()
|
|
127
|
+
for _, opt := range opts {
|
|
128
|
+
opt(cfg)
|
|
129
|
+
}
|
|
130
|
+
return NewOrderService(cfg)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// t.Cleanup() for automatic teardown
|
|
134
|
+
func setupTestDB(t *testing.T) *sql.DB {
|
|
135
|
+
t.Helper()
|
|
136
|
+
db, err := sql.Open("sqlite3", ":memory:")
|
|
137
|
+
require.NoError(t, err)
|
|
138
|
+
t.Cleanup(func() { db.Close() })
|
|
139
|
+
return db
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Subtests for Parallel Execution
|
|
144
|
+
|
|
145
|
+
```go
|
|
146
|
+
func TestOrderService(t *testing.T) {
|
|
147
|
+
t.Run("ProcessOrder", func(t *testing.T) {
|
|
148
|
+
t.Run("with valid order", func(t *testing.T) {
|
|
149
|
+
t.Parallel()
|
|
150
|
+
// ... test body
|
|
151
|
+
})
|
|
152
|
+
t.Run("with invalid order", func(t *testing.T) {
|
|
153
|
+
t.Parallel()
|
|
154
|
+
// ... test body
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Error Testing
|
|
161
|
+
|
|
162
|
+
```go
|
|
163
|
+
// Check specific error
|
|
164
|
+
func TestDivide_ByZero_ReturnsError(t *testing.T) {
|
|
165
|
+
_, err := Divide(1, 0)
|
|
166
|
+
assert.ErrorIs(t, err, ErrDivideByZero)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check error type
|
|
170
|
+
func TestParse_InvalidInput_ReturnsParseError(t *testing.T) {
|
|
171
|
+
_, err := Parse("bad input")
|
|
172
|
+
var parseErr *ParseError
|
|
173
|
+
assert.ErrorAs(t, err, &parseErr)
|
|
174
|
+
assert.Equal(t, "bad input", parseErr.Input)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check error message
|
|
178
|
+
func TestValidate_EmptyName_ReturnsErrorMessage(t *testing.T) {
|
|
179
|
+
err := Validate(User{Name: ""})
|
|
180
|
+
assert.EqualError(t, err, "name cannot be empty")
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Mocking
|
|
185
|
+
|
|
186
|
+
### testify/mock
|
|
187
|
+
|
|
188
|
+
```go
|
|
189
|
+
type MockOrderRepository struct {
|
|
190
|
+
mock.Mock
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func (m *MockOrderRepository) Save(ctx context.Context, order *Order) (*Order, error) {
|
|
194
|
+
args := m.Called(ctx, order)
|
|
195
|
+
return args.Get(0).(*Order), args.Error(1)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Usage
|
|
199
|
+
repo := new(MockOrderRepository)
|
|
200
|
+
repo.On("Save", mock.Anything, mock.Anything).Return(&Order{ID: "123"}, nil)
|
|
201
|
+
// ... use repo ...
|
|
202
|
+
repo.AssertExpectations(t)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Interface-based fakes (preferred for simple cases)
|
|
206
|
+
|
|
207
|
+
```go
|
|
208
|
+
// Define a minimal interface where you use it
|
|
209
|
+
type orderSaver interface {
|
|
210
|
+
Save(ctx context.Context, order *Order) (*Order, error)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fake implementation in test file
|
|
214
|
+
type fakeOrderSaver struct {
|
|
215
|
+
saved []*Order
|
|
216
|
+
err error
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func (f *fakeOrderSaver) Save(_ context.Context, order *Order) (*Order, error) {
|
|
220
|
+
if f.err != nil {
|
|
221
|
+
return nil, f.err
|
|
222
|
+
}
|
|
223
|
+
f.saved = append(f.saved, order)
|
|
224
|
+
return &Order{ID: "123"}, nil
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### What NOT to mock
|
|
229
|
+
|
|
230
|
+
- Value types, structs used as data containers
|
|
231
|
+
- Pure functions with no side effects
|
|
232
|
+
- The package under test itself
|
|
233
|
+
- Standard library types (use real `bytes.Buffer`, etc.)
|
|
234
|
+
|
|
235
|
+
Mock only at system boundaries: external APIs, databases, file system, time.
|
|
236
|
+
|
|
237
|
+
## File Conventions
|
|
238
|
+
|
|
239
|
+
- `*_test.go` in the same package (white-box) or `_test` package (black-box)
|
|
240
|
+
- `go test ./...` to run all tests
|
|
241
|
+
- `testdata/` for test fixtures (ignored by Go tooling)
|
|
242
|
+
- `go test -race ./...` to detect race conditions
|
|
243
|
+
- `go test -cover ./...` for coverage
|
|
244
|
+
|
|
245
|
+
## Authoritative Sources
|
|
246
|
+
|
|
247
|
+
- testing package: https://pkg.go.dev/testing
|
|
248
|
+
- testify: https://github.com/stretchr/testify
|
|
249
|
+
- gomock: https://github.com/uber-go/mock
|
|
250
|
+
- Kent Beck — Canon TDD: https://tidyfirst.substack.com/p/canon-tdd
|
|
251
|
+
- Martin Fowler — Mocks Aren't Stubs: https://martinfowler.com/articles/mocksArentStubs.html
|
|
252
|
+
|
|
253
|
+
## Reference Materials
|
|
254
|
+
|
|
255
|
+
- **[Anti-Patterns](reference/anti-patterns.md)** — Common testing mistakes and how to fix them
|
|
256
|
+
- **[TDD Workflow Patterns](reference/tdd-workflow-patterns.md)** — Red-Green-Refactor, Transformation Priority Premise, when to use TDD
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Testing Anti-Patterns
|
|
2
|
+
|
|
3
|
+
Common testing mistakes that reduce test value and increase maintenance cost. These are language-agnostic — they apply to any test framework.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [The Liar](#the-liar)
|
|
8
|
+
- [The Giant](#the-giant)
|
|
9
|
+
- [Excessive Setup](#excessive-setup)
|
|
10
|
+
- [The Slow Poke](#the-slow-poke)
|
|
11
|
+
- [The Peeping Tom](#the-peeping-tom)
|
|
12
|
+
- [The Mockery](#the-mockery)
|
|
13
|
+
- [The Inspector](#the-inspector)
|
|
14
|
+
- [The Flaky Test](#the-flaky-test)
|
|
15
|
+
- [The Cargo Culter](#the-cargo-culter)
|
|
16
|
+
- [The Hard Test](#the-hard-test)
|
|
17
|
+
|
|
18
|
+
## The Liar
|
|
19
|
+
|
|
20
|
+
**What it is:** A test that passes but doesn't actually verify the behavior it claims to test. It gives false confidence.
|
|
21
|
+
|
|
22
|
+
**How to spot it:**
|
|
23
|
+
- Test name says "validates input" but assertions only check the return type
|
|
24
|
+
- Assertions are too loose (`assert result is not None` instead of checking the actual value)
|
|
25
|
+
- Test catches exceptions broadly and passes regardless
|
|
26
|
+
|
|
27
|
+
**Fix:** Ensure assertions directly verify the specific behavior described in the test name. Every assertion should fail if the behavior breaks.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# Bad — passes even if discount logic is completely wrong
|
|
31
|
+
def test_apply_discount():
|
|
32
|
+
result = apply_discount(100, 10)
|
|
33
|
+
assert result is not None
|
|
34
|
+
|
|
35
|
+
# Good — fails if the calculation is wrong
|
|
36
|
+
def test_apply_discount_with_10_percent_returns_90():
|
|
37
|
+
result = apply_discount(100, 10)
|
|
38
|
+
assert result == 90.0
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The Giant
|
|
42
|
+
|
|
43
|
+
**What it is:** A single test that verifies too many things. When it fails, you can't tell which behavior broke.
|
|
44
|
+
|
|
45
|
+
**How to spot it:**
|
|
46
|
+
- Test has more than 8-10 assertions
|
|
47
|
+
- Test name uses "and" (e.g., "creates user and sends email and updates cache")
|
|
48
|
+
- Multiple Act phases in one test
|
|
49
|
+
|
|
50
|
+
**Fix:** Split into focused tests, each verifying one logical concept. Multiple assertions are fine if they verify aspects of the same behavior.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Bad — three unrelated behaviors in one test
|
|
54
|
+
test('user registration works', () => {
|
|
55
|
+
const user = register({ name: 'Alice', email: 'alice@test.com' });
|
|
56
|
+
expect(user.id).toBeDefined();
|
|
57
|
+
expect(emailService.send).toHaveBeenCalled();
|
|
58
|
+
expect(cache.set).toHaveBeenCalledWith(`user:${user.id}`, user);
|
|
59
|
+
expect(auditLog.entries).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Good — separate tests for each behavior
|
|
63
|
+
test('register with valid data creates user with id', () => { ... });
|
|
64
|
+
test('register with valid data sends welcome email', () => { ... });
|
|
65
|
+
test('register with valid data caches the user', () => { ... });
|
|
66
|
+
test('register with valid data writes audit log entry', () => { ... });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Excessive Setup
|
|
70
|
+
|
|
71
|
+
**What it is:** Tests that require dozens of lines of setup before the actual test logic. Often signals that the code under test has too many dependencies.
|
|
72
|
+
|
|
73
|
+
**How to spot it:**
|
|
74
|
+
- Arrange section is 20+ lines
|
|
75
|
+
- Multiple mocks configured with complex behaviors
|
|
76
|
+
- Shared setup methods that configure things most tests don't need
|
|
77
|
+
|
|
78
|
+
**Fix:** Use factory methods/builders for test data. Consider whether the code under test needs refactoring to reduce dependencies. Only set up what the specific test needs.
|
|
79
|
+
|
|
80
|
+
```go
|
|
81
|
+
// Bad — every test sets up the entire world
|
|
82
|
+
func TestProcessOrder(t *testing.T) {
|
|
83
|
+
db := setupDatabase()
|
|
84
|
+
cache := setupCache()
|
|
85
|
+
logger := setupLogger()
|
|
86
|
+
emailClient := setupEmailClient()
|
|
87
|
+
validator := NewValidator(db)
|
|
88
|
+
processor := NewProcessor(cache)
|
|
89
|
+
service := NewOrderService(db, cache, logger, emailClient, validator, processor)
|
|
90
|
+
// ... 10 more lines of setup
|
|
91
|
+
result, err := service.ProcessOrder(ctx, order)
|
|
92
|
+
assert.NoError(t, err)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Good — factory method hides irrelevant details
|
|
96
|
+
func TestProcessOrder_WithValidOrder_Succeeds(t *testing.T) {
|
|
97
|
+
service := newTestOrderService(t)
|
|
98
|
+
result, err := service.ProcessOrder(ctx, validOrder())
|
|
99
|
+
assert.NoError(t, err)
|
|
100
|
+
assert.Equal(t, "processed", result.Status)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The Slow Poke
|
|
105
|
+
|
|
106
|
+
**What it is:** Tests that are slow because they use real I/O, network calls, or sleeps. Slow tests get run less frequently and slow down the feedback loop.
|
|
107
|
+
|
|
108
|
+
**How to spot it:**
|
|
109
|
+
- `time.Sleep()`, `Thread.sleep()`, `setTimeout` in tests
|
|
110
|
+
- Real HTTP calls, database connections, file system operations
|
|
111
|
+
- Test suite takes more than a few seconds for unit tests
|
|
112
|
+
|
|
113
|
+
**Fix:** Mock external dependencies. Use fake implementations for I/O. Replace time-based waits with event-based synchronization.
|
|
114
|
+
|
|
115
|
+
## The Peeping Tom
|
|
116
|
+
|
|
117
|
+
**What it is:** Tests that access private/internal state to verify behavior instead of testing through the public interface.
|
|
118
|
+
|
|
119
|
+
**How to spot it:**
|
|
120
|
+
- Reflection to access private fields
|
|
121
|
+
- Testing internal method calls instead of observable results
|
|
122
|
+
- Assertions on implementation details (internal data structures, private counters)
|
|
123
|
+
|
|
124
|
+
**Fix:** Test through the public API. If you can't verify behavior through the public interface, the class may need a design change (e.g., expose a query method or extract a collaborator).
|
|
125
|
+
|
|
126
|
+
## The Mockery
|
|
127
|
+
|
|
128
|
+
**What it is:** Tests that mock so heavily that they're testing mock configurations rather than real behavior. Every dependency is mocked, including simple value objects.
|
|
129
|
+
|
|
130
|
+
**How to spot it:**
|
|
131
|
+
- More mock setup lines than actual test logic
|
|
132
|
+
- Mocking concrete classes, value objects, or data structures
|
|
133
|
+
- Test passes but the real system fails because mocks don't match reality
|
|
134
|
+
|
|
135
|
+
**Fix:** Only mock at system boundaries (external services, databases, clocks). Use real implementations for in-process collaborators when practical.
|
|
136
|
+
|
|
137
|
+
## The Inspector
|
|
138
|
+
|
|
139
|
+
**What it is:** Tests that verify exact method calls and their order rather than outcomes. They break whenever the implementation changes, even if behavior is preserved.
|
|
140
|
+
|
|
141
|
+
**How to spot it:**
|
|
142
|
+
- `verify(mock, times(1)).method()` for every mock interaction
|
|
143
|
+
- Assertions on call order
|
|
144
|
+
- Test breaks when you refactor without changing behavior
|
|
145
|
+
|
|
146
|
+
**Fix:** Verify state (the result) rather than interactions (how it got there). Only verify interactions for side effects that ARE the behavior (e.g., "email was sent").
|
|
147
|
+
|
|
148
|
+
```java
|
|
149
|
+
// Bad — breaks if implementation changes sort algorithm
|
|
150
|
+
verify(sorter, times(1)).quickSort(any());
|
|
151
|
+
verify(sorter, never()).mergeSort(any());
|
|
152
|
+
|
|
153
|
+
// Good — verifies the outcome
|
|
154
|
+
assertThat(result).isSortedAccordingTo(naturalOrder());
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## The Flaky Test
|
|
158
|
+
|
|
159
|
+
**What it is:** Tests that pass and fail intermittently without code changes. They erode trust in the test suite.
|
|
160
|
+
|
|
161
|
+
**Common causes:**
|
|
162
|
+
- Time-dependent logic (`new Date()`, `time.Now()`)
|
|
163
|
+
- Random data without fixed seeds
|
|
164
|
+
- Shared mutable state between tests
|
|
165
|
+
- Race conditions in async tests
|
|
166
|
+
- Dependency on test execution order
|
|
167
|
+
|
|
168
|
+
**Fix:** Inject time as a dependency. Use fixed seeds for randomness. Ensure test isolation. Use proper async synchronization.
|
|
169
|
+
|
|
170
|
+
## The Cargo Culter
|
|
171
|
+
|
|
172
|
+
**What it is:** Writing tests to hit a coverage percentage target rather than to verify behavior. The tests exist to satisfy a metric, not to provide confidence.
|
|
173
|
+
|
|
174
|
+
**How to spot it:**
|
|
175
|
+
- Tests that assert trivially obvious things (e.g., `assert user.name == user.name`)
|
|
176
|
+
- Every private method has a corresponding test accessed via reflection
|
|
177
|
+
- 100% coverage but bugs still escape to production
|
|
178
|
+
- Test suite takes minutes to pass but developers don't trust it
|
|
179
|
+
|
|
180
|
+
**Fix:** Coverage is a diagnostic tool, not a goal. Use it to find untested gaps, not as a number to optimize. High 80s–90% emerges naturally from disciplined TDD. A test that only exists to push coverage up is worse than no test — it adds maintenance cost without adding confidence.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# Bad — written for coverage, not for confidence
|
|
184
|
+
def test_user_has_name():
|
|
185
|
+
user = User(name="Alice")
|
|
186
|
+
assert user.name is not None # This verifies nothing meaningful
|
|
187
|
+
|
|
188
|
+
# Good — written to verify a business rule
|
|
189
|
+
def test_user_with_empty_name_raises_validation_error():
|
|
190
|
+
with pytest.raises(ValidationError, match="name cannot be empty"):
|
|
191
|
+
User(name="")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
> See: https://martinfowler.com/bliki/TestCoverage.html
|
|
195
|
+
|
|
196
|
+
## The Hard Test
|
|
197
|
+
|
|
198
|
+
**What it is:** Not an anti-pattern in the test itself, but a signal from the test about the production code. When a test is painful, complex, or requires elaborate setup, the production code has a design problem.
|
|
199
|
+
|
|
200
|
+
**How to spot it:**
|
|
201
|
+
- Need to mock 5+ dependencies to test one class
|
|
202
|
+
- Need to access private internals to verify behavior
|
|
203
|
+
- Test requires a complex sequence of operations just to get to the state under test
|
|
204
|
+
- You find yourself thinking "testing this would be too hard"
|
|
205
|
+
|
|
206
|
+
**What it signals:**
|
|
207
|
+
- Too many responsibilities in one class (SRP violation)
|
|
208
|
+
- Hidden dependencies or tight coupling
|
|
209
|
+
- Poor separation of concerns
|
|
210
|
+
- Untestable architecture (e.g., side effects embedded in business logic)
|
|
211
|
+
|
|
212
|
+
**Fix:** Resist the urge to skip the test or work around it with clever mocking. Instead, fix the production code design. Extract classes, inject dependencies, separate concerns. A hard test is a free design review — take the feedback.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# Hard to test — service does too much
|
|
216
|
+
class OrderService:
|
|
217
|
+
def process(self, order):
|
|
218
|
+
db = Database() # hidden dependency
|
|
219
|
+
email = EmailClient() # hidden dependency
|
|
220
|
+
self._validate(order)
|
|
221
|
+
db.save(order)
|
|
222
|
+
email.send_confirmation(order)
|
|
223
|
+
self._update_inventory(order) # another responsibility
|
|
224
|
+
|
|
225
|
+
# Easy to test — dependencies explicit, concerns separated
|
|
226
|
+
class OrderService:
|
|
227
|
+
def __init__(self, repo: OrderRepository, notifier: Notifier):
|
|
228
|
+
self._repo = repo
|
|
229
|
+
self._notifier = notifier
|
|
230
|
+
|
|
231
|
+
def process(self, order: Order) -> OrderResult:
|
|
232
|
+
self._validate(order)
|
|
233
|
+
saved = self._repo.save(order)
|
|
234
|
+
self._notifier.notify(saved)
|
|
235
|
+
return saved
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Further Reading
|
|
241
|
+
|
|
242
|
+
- xUnit Patterns (Meszaros): http://xunitpatterns.com
|
|
243
|
+
- Codepipes testing anti-patterns: https://blog.codepipes.com/testing/software-testing-antipatterns.html
|
|
244
|
+
- Google SWE Book — Test Doubles: https://abseil.io/resources/swe-book/html/ch13.html
|