@grimoire-cc/cli 0.6.3 → 0.7.1
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/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/static/log-viewer.html +946 -690
- package/dist/static/static/log-viewer.html +946 -690
- package/package.json +1 -1
- package/packs/dev-pack/agents/gr.code-reviewer.md +286 -0
- package/packs/dev-pack/agents/gr.tdd-specialist.md +44 -0
- package/packs/dev-pack/grimoire.json +55 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/SKILL.md +247 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/anti-patterns.md +166 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/language-frameworks.md +388 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/tdd-workflow-patterns.md +135 -0
- package/packs/docs-pack/grimoire.json +30 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/SKILL.md +278 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/references/audit-checklist.md +48 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/references/tier2-template.md +129 -0
- package/packs/essentials-pack/agents/gr.fact-checker.md +202 -0
- package/packs/essentials-pack/grimoire.json +12 -0
- package/packs/meta-pack/grimoire.json +72 -0
- package/packs/meta-pack/skills/gr.context-file-guide/SKILL.md +201 -0
- package/packs/meta-pack/skills/gr.context-file-guide/scripts/validate-context-file.sh +29 -0
- package/packs/meta-pack/skills/gr.readme-guide/SKILL.md +362 -0
- package/packs/meta-pack/skills/gr.skill-developer/SKILL.md +321 -0
- package/packs/meta-pack/skills/gr.skill-developer/examples/brand-guidelines.md +94 -0
- package/packs/meta-pack/skills/gr.skill-developer/examples/financial-analysis.md +85 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/best-practices.md +410 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/file-organization.md +452 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/patterns.md +459 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/yaml-spec.md +214 -0
- package/packs/meta-pack/skills/gr.skill-developer/scripts/create-skill.sh +210 -0
- package/packs/meta-pack/skills/gr.skill-developer/scripts/validate-skill.py +520 -0
- package/packs/meta-pack/skills/gr.skill-developer/templates/basic-skill.md +94 -0
- package/packs/meta-pack/skills/gr.skill-developer/templates/domain-skill.md +108 -0
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
|
|
16
|
+
## The Liar
|
|
17
|
+
|
|
18
|
+
**What it is:** A test that passes but doesn't actually verify the behavior it claims to test. It gives false confidence.
|
|
19
|
+
|
|
20
|
+
**How to spot it:**
|
|
21
|
+
- Test name says "validates input" but assertions only check the return type
|
|
22
|
+
- Assertions are too loose (`assert result is not None` instead of checking the actual value)
|
|
23
|
+
- Test catches exceptions broadly and passes regardless
|
|
24
|
+
|
|
25
|
+
**Fix:** Ensure assertions directly verify the specific behavior described in the test name. Every assertion should fail if the behavior breaks.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# Bad — passes even if discount logic is completely wrong
|
|
29
|
+
def test_apply_discount():
|
|
30
|
+
result = apply_discount(100, 10)
|
|
31
|
+
assert result is not None
|
|
32
|
+
|
|
33
|
+
# Good — fails if the calculation is wrong
|
|
34
|
+
def test_apply_discount_with_10_percent_returns_90():
|
|
35
|
+
result = apply_discount(100, 10)
|
|
36
|
+
assert result == 90.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## The Giant
|
|
40
|
+
|
|
41
|
+
**What it is:** A single test that verifies too many things. When it fails, you can't tell which behavior broke.
|
|
42
|
+
|
|
43
|
+
**How to spot it:**
|
|
44
|
+
- Test has more than 8-10 assertions
|
|
45
|
+
- Test name uses "and" (e.g., "creates user and sends email and updates cache")
|
|
46
|
+
- Multiple Act phases in one test
|
|
47
|
+
|
|
48
|
+
**Fix:** Split into focused tests, each verifying one logical concept. Multiple assertions are fine if they verify aspects of the same behavior.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Bad — three unrelated behaviors in one test
|
|
52
|
+
test('user registration works', () => {
|
|
53
|
+
const user = register({ name: 'Alice', email: 'alice@test.com' });
|
|
54
|
+
expect(user.id).toBeDefined();
|
|
55
|
+
expect(emailService.send).toHaveBeenCalled();
|
|
56
|
+
expect(cache.set).toHaveBeenCalledWith(`user:${user.id}`, user);
|
|
57
|
+
expect(auditLog.entries).toHaveLength(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Good — separate tests for each behavior
|
|
61
|
+
test('register with valid data creates user with id', () => { ... });
|
|
62
|
+
test('register with valid data sends welcome email', () => { ... });
|
|
63
|
+
test('register with valid data caches the user', () => { ... });
|
|
64
|
+
test('register with valid data writes audit log entry', () => { ... });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Excessive Setup
|
|
68
|
+
|
|
69
|
+
**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.
|
|
70
|
+
|
|
71
|
+
**How to spot it:**
|
|
72
|
+
- Arrange section is 20+ lines
|
|
73
|
+
- Multiple mocks configured with complex behaviors
|
|
74
|
+
- Shared setup methods that configure things most tests don't need
|
|
75
|
+
|
|
76
|
+
**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.
|
|
77
|
+
|
|
78
|
+
```go
|
|
79
|
+
// Bad — every test sets up the entire world
|
|
80
|
+
func TestProcessOrder(t *testing.T) {
|
|
81
|
+
db := setupDatabase()
|
|
82
|
+
cache := setupCache()
|
|
83
|
+
logger := setupLogger()
|
|
84
|
+
emailClient := setupEmailClient()
|
|
85
|
+
validator := NewValidator(db)
|
|
86
|
+
processor := NewProcessor(cache)
|
|
87
|
+
service := NewOrderService(db, cache, logger, emailClient, validator, processor)
|
|
88
|
+
// ... 10 more lines of setup
|
|
89
|
+
result, err := service.ProcessOrder(ctx, order)
|
|
90
|
+
assert.NoError(t, err)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Good — factory method hides irrelevant details
|
|
94
|
+
func TestProcessOrder_WithValidOrder_Succeeds(t *testing.T) {
|
|
95
|
+
service := newTestOrderService(t)
|
|
96
|
+
result, err := service.ProcessOrder(ctx, validOrder())
|
|
97
|
+
assert.NoError(t, err)
|
|
98
|
+
assert.Equal(t, "processed", result.Status)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## The Slow Poke
|
|
103
|
+
|
|
104
|
+
**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.
|
|
105
|
+
|
|
106
|
+
**How to spot it:**
|
|
107
|
+
- `time.Sleep()`, `Thread.sleep()`, `setTimeout` in tests
|
|
108
|
+
- Real HTTP calls, database connections, file system operations
|
|
109
|
+
- Test suite takes more than a few seconds for unit tests
|
|
110
|
+
|
|
111
|
+
**Fix:** Mock external dependencies. Use fake implementations for I/O. Replace time-based waits with event-based synchronization.
|
|
112
|
+
|
|
113
|
+
## The Peeping Tom
|
|
114
|
+
|
|
115
|
+
**What it is:** Tests that access private/internal state to verify behavior instead of testing through the public interface.
|
|
116
|
+
|
|
117
|
+
**How to spot it:**
|
|
118
|
+
- Reflection to access private fields
|
|
119
|
+
- Testing internal method calls instead of observable results
|
|
120
|
+
- Assertions on implementation details (internal data structures, private counters)
|
|
121
|
+
|
|
122
|
+
**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).
|
|
123
|
+
|
|
124
|
+
## The Mockery
|
|
125
|
+
|
|
126
|
+
**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.
|
|
127
|
+
|
|
128
|
+
**How to spot it:**
|
|
129
|
+
- More mock setup lines than actual test logic
|
|
130
|
+
- Mocking concrete classes, value objects, or data structures
|
|
131
|
+
- Test passes but the real system fails because mocks don't match reality
|
|
132
|
+
|
|
133
|
+
**Fix:** Only mock at system boundaries (external services, databases, clocks). Use real implementations for in-process collaborators when practical.
|
|
134
|
+
|
|
135
|
+
## The Inspector
|
|
136
|
+
|
|
137
|
+
**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.
|
|
138
|
+
|
|
139
|
+
**How to spot it:**
|
|
140
|
+
- `verify(mock, times(1)).method()` for every mock interaction
|
|
141
|
+
- Assertions on call order
|
|
142
|
+
- Test breaks when you refactor without changing behavior
|
|
143
|
+
|
|
144
|
+
**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").
|
|
145
|
+
|
|
146
|
+
```java
|
|
147
|
+
// Bad — breaks if implementation changes sort algorithm
|
|
148
|
+
verify(sorter, times(1)).quickSort(any());
|
|
149
|
+
verify(sorter, never()).mergeSort(any());
|
|
150
|
+
|
|
151
|
+
// Good — verifies the outcome
|
|
152
|
+
assertThat(result).isSortedAccordingTo(naturalOrder());
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## The Flaky Test
|
|
156
|
+
|
|
157
|
+
**What it is:** Tests that pass and fail intermittently without code changes. They erode trust in the test suite.
|
|
158
|
+
|
|
159
|
+
**Common causes:**
|
|
160
|
+
- Time-dependent logic (`new Date()`, `time.Now()`)
|
|
161
|
+
- Random data without fixed seeds
|
|
162
|
+
- Shared mutable state between tests
|
|
163
|
+
- Race conditions in async tests
|
|
164
|
+
- Dependency on test execution order
|
|
165
|
+
|
|
166
|
+
**Fix:** Inject time as a dependency. Use fixed seeds for randomness. Ensure test isolation. Use proper async synchronization.
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Language & Framework Reference
|
|
2
|
+
|
|
3
|
+
Quick-reference for each language's test ecosystem. Use this to apply framework-specific patterns after detecting the project's language.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [JavaScript / TypeScript](#javascript--typescript)
|
|
8
|
+
- [Python](#python)
|
|
9
|
+
- [Go](#go)
|
|
10
|
+
- [Rust](#rust)
|
|
11
|
+
- [Java / Kotlin](#java--kotlin)
|
|
12
|
+
- [C# / .NET](#c--net)
|
|
13
|
+
- [Ruby](#ruby)
|
|
14
|
+
|
|
15
|
+
## JavaScript / TypeScript
|
|
16
|
+
|
|
17
|
+
### Frameworks
|
|
18
|
+
|
|
19
|
+
| Framework | When to Use | Key Feature |
|
|
20
|
+
|-----------|-------------|-------------|
|
|
21
|
+
| **Vitest** | Vite projects, new TS projects | Fast, ESM-native, Jest-compatible API |
|
|
22
|
+
| **Jest** | React (CRA), existing Jest projects | Mature ecosystem, wide adoption |
|
|
23
|
+
| **Mocha + Chai** | Legacy projects, custom setups | Flexible, pluggable |
|
|
24
|
+
| **Node test runner** | Node.js 20+, minimal deps | Built-in, zero dependencies |
|
|
25
|
+
|
|
26
|
+
### Vitest / Jest Patterns
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
30
|
+
|
|
31
|
+
describe('OrderService', () => {
|
|
32
|
+
let mockRepo: MockedObject<OrderRepository>;
|
|
33
|
+
let service: OrderService;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mockRepo = { save: vi.fn(), findById: vi.fn() };
|
|
37
|
+
service = new OrderService(mockRepo);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('processOrder with valid order saves and returns id', async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const order = createValidOrder();
|
|
43
|
+
mockRepo.save.mockResolvedValue({ id: '123' });
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const result = await service.processOrder(order);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(result.id).toBe('123');
|
|
50
|
+
expect(mockRepo.save).toHaveBeenCalledWith(order);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('processOrder with invalid order throws ValidationError', async () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const order = createInvalidOrder();
|
|
56
|
+
|
|
57
|
+
// Act & Assert
|
|
58
|
+
await expect(service.processOrder(order)).rejects.toThrow(ValidationError);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### File Conventions
|
|
64
|
+
|
|
65
|
+
- `*.test.ts` / `*.spec.ts` (co-located or in `__tests__/`)
|
|
66
|
+
- `vitest.config.ts` or `jest.config.ts` for configuration
|
|
67
|
+
|
|
68
|
+
## Python
|
|
69
|
+
|
|
70
|
+
### Frameworks
|
|
71
|
+
|
|
72
|
+
| Framework | When to Use | Key Feature |
|
|
73
|
+
|-----------|-------------|-------------|
|
|
74
|
+
| **pytest** | Default choice, most projects | Fixtures, parametrize, plugins |
|
|
75
|
+
| **unittest** | stdlib only, legacy projects | Built-in, class-based |
|
|
76
|
+
|
|
77
|
+
### pytest Patterns
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import pytest
|
|
81
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def mock_repo():
|
|
85
|
+
repo = Mock(spec=OrderRepository)
|
|
86
|
+
repo.save = AsyncMock(return_value=Order(id="123"))
|
|
87
|
+
return repo
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def service(mock_repo):
|
|
91
|
+
return OrderService(repository=mock_repo)
|
|
92
|
+
|
|
93
|
+
async def test_process_order_with_valid_order_returns_id(service, mock_repo):
|
|
94
|
+
# Arrange
|
|
95
|
+
order = create_valid_order()
|
|
96
|
+
|
|
97
|
+
# Act
|
|
98
|
+
result = await service.process_order(order)
|
|
99
|
+
|
|
100
|
+
# Assert
|
|
101
|
+
assert result.id == "123"
|
|
102
|
+
mock_repo.save.assert_called_once_with(order)
|
|
103
|
+
|
|
104
|
+
async def test_process_order_with_invalid_order_raises_validation_error(service):
|
|
105
|
+
# Arrange
|
|
106
|
+
order = create_invalid_order()
|
|
107
|
+
|
|
108
|
+
# Act & Assert
|
|
109
|
+
with pytest.raises(ValidationError, match="items cannot be empty"):
|
|
110
|
+
await service.process_order(order)
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize("discount,expected", [
|
|
113
|
+
(0, 100.0),
|
|
114
|
+
(10, 90.0),
|
|
115
|
+
(50, 50.0),
|
|
116
|
+
])
|
|
117
|
+
def test_apply_discount_calculates_correctly(discount, expected):
|
|
118
|
+
assert apply_discount(100.0, discount) == expected
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### File Conventions
|
|
122
|
+
|
|
123
|
+
- `test_*.py` or `*_test.py` in `tests/` directory
|
|
124
|
+
- `conftest.py` for shared fixtures
|
|
125
|
+
- `pytest.ini` or `[tool.pytest.ini_options]` in `pyproject.toml`
|
|
126
|
+
|
|
127
|
+
## Go
|
|
128
|
+
|
|
129
|
+
### Frameworks
|
|
130
|
+
|
|
131
|
+
| Framework | When to Use | Key Feature |
|
|
132
|
+
|-----------|-------------|-------------|
|
|
133
|
+
| **testing** (stdlib) | Always available | Built-in, no dependencies |
|
|
134
|
+
| **testify** | Assertions + mocking | `assert`, `require`, `mock` packages |
|
|
135
|
+
| **gomock** | Interface mocking | Code generation, strict expectations |
|
|
136
|
+
|
|
137
|
+
### Go Patterns
|
|
138
|
+
|
|
139
|
+
```go
|
|
140
|
+
func TestProcessOrder_WithValidOrder_ReturnsID(t *testing.T) {
|
|
141
|
+
// Arrange
|
|
142
|
+
repo := new(MockOrderRepository)
|
|
143
|
+
repo.On("Save", mock.Anything).Return(&Order{ID: "123"}, nil)
|
|
144
|
+
service := NewOrderService(repo)
|
|
145
|
+
|
|
146
|
+
// Act
|
|
147
|
+
result, err := service.ProcessOrder(context.Background(), validOrder)
|
|
148
|
+
|
|
149
|
+
// Assert
|
|
150
|
+
require.NoError(t, err)
|
|
151
|
+
assert.Equal(t, "123", result.ID)
|
|
152
|
+
repo.AssertExpectations(t)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func TestProcessOrder_WithInvalidOrder_ReturnsError(t *testing.T) {
|
|
156
|
+
// Arrange
|
|
157
|
+
service := NewOrderService(nil)
|
|
158
|
+
|
|
159
|
+
// Act
|
|
160
|
+
_, err := service.ProcessOrder(context.Background(), invalidOrder)
|
|
161
|
+
|
|
162
|
+
// Assert
|
|
163
|
+
assert.ErrorIs(t, err, ErrValidation)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Table-driven tests (Go idiom)
|
|
167
|
+
func TestApplyDiscount(t *testing.T) {
|
|
168
|
+
tests := []struct {
|
|
169
|
+
name string
|
|
170
|
+
price float64
|
|
171
|
+
discount int
|
|
172
|
+
expected float64
|
|
173
|
+
}{
|
|
174
|
+
{"no discount", 100.0, 0, 100.0},
|
|
175
|
+
{"10 percent", 100.0, 10, 90.0},
|
|
176
|
+
{"50 percent", 100.0, 50, 50.0},
|
|
177
|
+
}
|
|
178
|
+
for _, tt := range tests {
|
|
179
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
180
|
+
result := ApplyDiscount(tt.price, tt.discount)
|
|
181
|
+
assert.Equal(t, tt.expected, result)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### File Conventions
|
|
188
|
+
|
|
189
|
+
- `*_test.go` in the same package (or `_test` package for black-box)
|
|
190
|
+
- `go test ./...` to run all tests
|
|
191
|
+
- `testdata/` for test fixtures
|
|
192
|
+
|
|
193
|
+
## Rust
|
|
194
|
+
|
|
195
|
+
### Patterns
|
|
196
|
+
|
|
197
|
+
```rust
|
|
198
|
+
#[cfg(test)]
|
|
199
|
+
mod tests {
|
|
200
|
+
use super::*;
|
|
201
|
+
use mockall::predicate::*;
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn process_order_with_valid_order_returns_id() {
|
|
205
|
+
// Arrange
|
|
206
|
+
let mut mock_repo = MockOrderRepository::new();
|
|
207
|
+
mock_repo.expect_save()
|
|
208
|
+
.returning(|_| Ok(Order { id: "123".into() }));
|
|
209
|
+
let service = OrderService::new(Box::new(mock_repo));
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
let result = service.process_order(&valid_order());
|
|
213
|
+
|
|
214
|
+
// Assert
|
|
215
|
+
assert_eq!(result.unwrap().id, "123");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn process_order_with_invalid_order_returns_error() {
|
|
220
|
+
// Arrange
|
|
221
|
+
let service = OrderService::new(Box::new(MockOrderRepository::new()));
|
|
222
|
+
|
|
223
|
+
// Act
|
|
224
|
+
let result = service.process_order(&invalid_order());
|
|
225
|
+
|
|
226
|
+
// Assert
|
|
227
|
+
assert!(matches!(result, Err(ServiceError::Validation(_))));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### File Conventions
|
|
233
|
+
|
|
234
|
+
- Tests in `#[cfg(test)] mod tests` at bottom of source file (unit tests)
|
|
235
|
+
- Integration tests in `tests/` directory
|
|
236
|
+
- `cargo test` to run all
|
|
237
|
+
|
|
238
|
+
## Java / Kotlin
|
|
239
|
+
|
|
240
|
+
### Frameworks
|
|
241
|
+
|
|
242
|
+
| Framework | When to Use | Key Feature |
|
|
243
|
+
|-----------|-------------|-------------|
|
|
244
|
+
| **JUnit 5** | Default choice | Modern, extensible, parameterized |
|
|
245
|
+
| **Mockito** | Mocking | Intuitive API, wide adoption |
|
|
246
|
+
| **AssertJ** | Fluent assertions | Readable, discoverable API |
|
|
247
|
+
|
|
248
|
+
### JUnit 5 + Mockito Patterns
|
|
249
|
+
|
|
250
|
+
```java
|
|
251
|
+
@ExtendWith(MockitoExtension.class)
|
|
252
|
+
class OrderServiceTest {
|
|
253
|
+
@Mock OrderRepository repository;
|
|
254
|
+
@InjectMocks OrderService service;
|
|
255
|
+
|
|
256
|
+
@Test
|
|
257
|
+
void processOrder_withValidOrder_returnsId() {
|
|
258
|
+
// Arrange
|
|
259
|
+
var order = createValidOrder();
|
|
260
|
+
when(repository.save(any())).thenReturn(new Order("123"));
|
|
261
|
+
|
|
262
|
+
// Act
|
|
263
|
+
var result = service.processOrder(order);
|
|
264
|
+
|
|
265
|
+
// Assert
|
|
266
|
+
assertThat(result.getId()).isEqualTo("123");
|
|
267
|
+
verify(repository).save(order);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Test
|
|
271
|
+
void processOrder_withInvalidOrder_throwsValidationException() {
|
|
272
|
+
// Arrange
|
|
273
|
+
var order = createInvalidOrder();
|
|
274
|
+
|
|
275
|
+
// Act & Assert
|
|
276
|
+
assertThatThrownBy(() -> service.processOrder(order))
|
|
277
|
+
.isInstanceOf(ValidationException.class)
|
|
278
|
+
.hasMessageContaining("items cannot be empty");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@ParameterizedTest
|
|
282
|
+
@CsvSource({"0, 100.0", "10, 90.0", "50, 50.0"})
|
|
283
|
+
void applyDiscount_calculatesCorrectly(int discount, double expected) {
|
|
284
|
+
assertThat(applyDiscount(100.0, discount)).isEqualTo(expected);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### File Conventions
|
|
290
|
+
|
|
291
|
+
- `src/test/java/` mirroring `src/main/java/` structure
|
|
292
|
+
- `*Test.java` suffix
|
|
293
|
+
- `mvn test` or `gradle test`
|
|
294
|
+
|
|
295
|
+
## C# / .NET
|
|
296
|
+
|
|
297
|
+
**If the `grimoire:dotnet-unit-testing` skill is available, defer to it for full C#/.NET guidance.** It provides comprehensive xUnit, TUnit, Moq, and NSubstitute patterns.
|
|
298
|
+
|
|
299
|
+
### Quick Reference (when dotnet skill is unavailable)
|
|
300
|
+
|
|
301
|
+
| Framework | When to Use |
|
|
302
|
+
|-----------|-------------|
|
|
303
|
+
| **xUnit** | Default, most universal |
|
|
304
|
+
| **NUnit** | If project already uses it |
|
|
305
|
+
| **MSTest** | Microsoft-first shops |
|
|
306
|
+
|
|
307
|
+
```csharp
|
|
308
|
+
public class OrderServiceTests
|
|
309
|
+
{
|
|
310
|
+
private readonly Mock<IOrderRepository> _mockRepo;
|
|
311
|
+
private readonly OrderService _sut;
|
|
312
|
+
|
|
313
|
+
public OrderServiceTests()
|
|
314
|
+
{
|
|
315
|
+
_mockRepo = new Mock<IOrderRepository>();
|
|
316
|
+
_sut = new OrderService(_mockRepo.Object);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
[Fact]
|
|
320
|
+
public async Task ProcessOrder_WithValidOrder_ReturnsId()
|
|
321
|
+
{
|
|
322
|
+
// Arrange
|
|
323
|
+
var order = CreateValidOrder();
|
|
324
|
+
_mockRepo.Setup(r => r.SaveAsync(It.IsAny<Order>()))
|
|
325
|
+
.ReturnsAsync(new Order { Id = "123" });
|
|
326
|
+
|
|
327
|
+
// Act
|
|
328
|
+
var result = await _sut.ProcessOrderAsync(order);
|
|
329
|
+
|
|
330
|
+
// Assert
|
|
331
|
+
Assert.Equal("123", result.Id);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### File Conventions
|
|
337
|
+
|
|
338
|
+
- `Tests/` or `*.Tests/` project mirroring source structure
|
|
339
|
+
- `*Tests.cs` suffix
|
|
340
|
+
- `dotnet test` to run
|
|
341
|
+
|
|
342
|
+
## Ruby
|
|
343
|
+
|
|
344
|
+
### Frameworks
|
|
345
|
+
|
|
346
|
+
| Framework | When to Use |
|
|
347
|
+
|-----------|-------------|
|
|
348
|
+
| **RSpec** | Default, most Ruby projects |
|
|
349
|
+
| **Minitest** | stdlib, Rails default |
|
|
350
|
+
|
|
351
|
+
### RSpec Patterns
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
RSpec.describe OrderService do
|
|
355
|
+
let(:repository) { instance_double(OrderRepository) }
|
|
356
|
+
let(:service) { described_class.new(repository: repository) }
|
|
357
|
+
|
|
358
|
+
describe '#process_order' do
|
|
359
|
+
context 'with a valid order' do
|
|
360
|
+
it 'returns the order id' do
|
|
361
|
+
# Arrange
|
|
362
|
+
order = build(:order, :valid)
|
|
363
|
+
allow(repository).to receive(:save).and_return(Order.new(id: '123'))
|
|
364
|
+
|
|
365
|
+
# Act
|
|
366
|
+
result = service.process_order(order)
|
|
367
|
+
|
|
368
|
+
# Assert
|
|
369
|
+
expect(result.id).to eq('123')
|
|
370
|
+
expect(repository).to have_received(:save).with(order)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
context 'with an invalid order' do
|
|
375
|
+
it 'raises ValidationError' do
|
|
376
|
+
order = build(:order, :invalid)
|
|
377
|
+
expect { service.process_order(order) }.to raise_error(ValidationError)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### File Conventions
|
|
385
|
+
|
|
386
|
+
- `spec/` directory mirroring `lib/` or `app/` structure
|
|
387
|
+
- `*_spec.rb` suffix
|
|
388
|
+
- `bundle exec rspec` to run
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# TDD Workflow Patterns
|
|
2
|
+
|
|
3
|
+
Guidance on the test-driven development process, when to apply it, and advanced techniques.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Red-Green-Refactor](#red-green-refactor)
|
|
8
|
+
- [Transformation Priority Premise](#transformation-priority-premise)
|
|
9
|
+
- [When to Use TDD](#when-to-use-tdd)
|
|
10
|
+
- [When TDD Is Less Effective](#when-tdd-is-less-effective)
|
|
11
|
+
- [BDD and ATDD Extensions](#bdd-and-atdd-extensions)
|
|
12
|
+
|
|
13
|
+
## Red-Green-Refactor
|
|
14
|
+
|
|
15
|
+
The core TDD cycle, repeated in small increments:
|
|
16
|
+
|
|
17
|
+
### 1. Red — Write a Failing Test
|
|
18
|
+
|
|
19
|
+
Write the smallest test that describes the next piece of behavior. The test MUST fail before you write any production code. A test that passes immediately provides no confidence.
|
|
20
|
+
|
|
21
|
+
**Rules:**
|
|
22
|
+
- Write only ONE test at a time
|
|
23
|
+
- The test should compile/parse but fail at the assertion
|
|
24
|
+
- If the test passes immediately, it's either trivial or testing existing behavior
|
|
25
|
+
|
|
26
|
+
### 2. Green — Make It Pass
|
|
27
|
+
|
|
28
|
+
Write the MINIMUM code to make the failing test pass. Do not add extra logic, handle cases not yet tested, or optimize.
|
|
29
|
+
|
|
30
|
+
**Rules:**
|
|
31
|
+
- Write the simplest code that makes the test pass
|
|
32
|
+
- It's OK to hardcode values initially — the next test will force generalization
|
|
33
|
+
- Do not add code for future tests
|
|
34
|
+
- All existing tests must still pass
|
|
35
|
+
|
|
36
|
+
### 3. Refactor — Clean Up
|
|
37
|
+
|
|
38
|
+
With all tests green, improve the code structure without changing behavior. Tests give you the safety net.
|
|
39
|
+
|
|
40
|
+
**Rules:**
|
|
41
|
+
- No new functionality during refactoring
|
|
42
|
+
- All tests must remain green after each refactoring step
|
|
43
|
+
- Remove duplication, improve naming, extract methods
|
|
44
|
+
- Refactor both production code AND test code
|
|
45
|
+
|
|
46
|
+
### Cycle Length
|
|
47
|
+
|
|
48
|
+
Each Red-Green-Refactor cycle should take 1-10 minutes. If you're spending more than 10 minutes in the Red or Green phase, the step is too large — break it down.
|
|
49
|
+
|
|
50
|
+
## Transformation Priority Premise
|
|
51
|
+
|
|
52
|
+
Kent Beck's insight: when going from Red to Green, prefer simpler transformations over complex ones. Listed from simplest to most complex:
|
|
53
|
+
|
|
54
|
+
1. **Constant** — return a hardcoded value
|
|
55
|
+
2. **Scalar** — replace constant with a variable
|
|
56
|
+
3. **Direct** — replace unconditional with conditional (if/else)
|
|
57
|
+
4. **Collection** — operate on a collection instead of a scalar
|
|
58
|
+
5. **Iteration** — add a loop
|
|
59
|
+
6. **Recursion** — add recursive call
|
|
60
|
+
7. **Assignment** — replace computed value with mutation
|
|
61
|
+
|
|
62
|
+
**Example — building FizzBuzz with TDD:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Test 1: input 1 → "1" Transformation: Constant
|
|
66
|
+
Test 2: input 2 → "2" Transformation: Scalar (use the input)
|
|
67
|
+
Test 3: input 3 → "Fizz" Transformation: Direct (add if)
|
|
68
|
+
Test 4: input 5 → "Buzz" Transformation: Direct (add another if)
|
|
69
|
+
Test 5: input 15 → "FizzBuzz" Transformation: Direct (add combined if)
|
|
70
|
+
Test 6: input 1-15 → full list Transformation: Iteration (generalize)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
By following this priority, you avoid over-engineering early and let the design emerge naturally from the tests.
|
|
74
|
+
|
|
75
|
+
## When to Use TDD
|
|
76
|
+
|
|
77
|
+
TDD is most valuable when:
|
|
78
|
+
|
|
79
|
+
- **Business logic** — Complex rules, calculations, state machines. TDD forces you to think through all cases before implementing.
|
|
80
|
+
- **Algorithm development** — Sorting, parsing, validation, transformation logic. Tests serve as a specification.
|
|
81
|
+
- **Bug fixes** — Write a test that reproduces the bug first (Red), then fix it (Green). This prevents regressions.
|
|
82
|
+
- **API/interface design** — Writing tests first helps you design interfaces from the consumer's perspective.
|
|
83
|
+
- **Refactoring** — Ensure tests exist before refactoring. If they don't, write characterization tests first, then refactor.
|
|
84
|
+
|
|
85
|
+
## When TDD Is Less Effective
|
|
86
|
+
|
|
87
|
+
TDD is not universally optimal. Use judgment:
|
|
88
|
+
|
|
89
|
+
- **UI/visual components** — Layout, styling, animations are hard to express as unit tests. Use visual regression testing or snapshot tests instead.
|
|
90
|
+
- **Exploratory/prototype code** — When you don't know what to build yet, writing tests first slows exploration. Spike first, then write tests.
|
|
91
|
+
- **Thin integration layers** — Simple pass-through code (e.g., a controller that calls a service) may not benefit from test-first approach. Integration tests are more valuable here.
|
|
92
|
+
- **Infrastructure/glue code** — Database migrations, config files, build scripts. Test these with integration or end-to-end tests.
|
|
93
|
+
- **External API wrappers** — Thin clients wrapping external APIs are better tested with integration tests against the real (or sandboxed) API.
|
|
94
|
+
|
|
95
|
+
For these cases, write tests AFTER the implementation (test-last), but still write them.
|
|
96
|
+
|
|
97
|
+
## BDD and ATDD Extensions
|
|
98
|
+
|
|
99
|
+
### Behavior-Driven Development (BDD)
|
|
100
|
+
|
|
101
|
+
BDD extends TDD by using natural language to describe behavior. Useful when tests need to be readable by non-developers.
|
|
102
|
+
|
|
103
|
+
**Given-When-Then** structure:
|
|
104
|
+
|
|
105
|
+
```gherkin
|
|
106
|
+
Given a cart with items totaling $100
|
|
107
|
+
When a 10% discount is applied
|
|
108
|
+
Then the total should be $90
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Maps to test code:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
def test_cart_with_10_percent_discount_totals_90():
|
|
115
|
+
# Given
|
|
116
|
+
cart = Cart(items=[Item(price=100)])
|
|
117
|
+
|
|
118
|
+
# When
|
|
119
|
+
cart.apply_discount(PercentageDiscount(10))
|
|
120
|
+
|
|
121
|
+
# Then
|
|
122
|
+
assert cart.total == 90.0
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Acceptance TDD (ATDD)
|
|
126
|
+
|
|
127
|
+
Write high-level acceptance tests before implementing a feature. These tests describe the feature from the user's perspective and drive the overall design. Unit tests (via TDD) then drive the implementation of each component.
|
|
128
|
+
|
|
129
|
+
**Flow:**
|
|
130
|
+
1. Write acceptance test (fails — Red)
|
|
131
|
+
2. Use TDD to implement components needed to pass it
|
|
132
|
+
3. Acceptance test passes (Green)
|
|
133
|
+
4. Refactor
|
|
134
|
+
|
|
135
|
+
ATDD is most valuable for features with clear acceptance criteria and when working with product owners or stakeholders.
|