ace-test 0.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.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/protocols/agent-sources/ace-test.yml +19 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-test.yml +19 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-test.yml +11 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-test.yml +19 -0
- data/CHANGELOG.md +169 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +12 -0
- data/handbook/agents/mock.ag.md +164 -0
- data/handbook/agents/profile-tests.ag.md +132 -0
- data/handbook/agents/test.ag.md +99 -0
- data/handbook/guides/SUMMARY.md +95 -0
- data/handbook/guides/embedded-testing-guide.g.md +261 -0
- data/handbook/guides/mocking-patterns.g.md +464 -0
- data/handbook/guides/quick-reference.g.md +46 -0
- data/handbook/guides/test-driven-development-cycle/meta-documentation.md +26 -0
- data/handbook/guides/test-driven-development-cycle/ruby-application.md +18 -0
- data/handbook/guides/test-driven-development-cycle/ruby-gem.md +19 -0
- data/handbook/guides/test-driven-development-cycle/rust-cli.md +18 -0
- data/handbook/guides/test-driven-development-cycle/rust-wasm-zed.md +19 -0
- data/handbook/guides/test-driven-development-cycle/typescript-nuxt.md +18 -0
- data/handbook/guides/test-driven-development-cycle/typescript-vue.md +19 -0
- data/handbook/guides/test-layer-decision.g.md +261 -0
- data/handbook/guides/test-mocking-patterns.g.md +414 -0
- data/handbook/guides/test-organization.g.md +140 -0
- data/handbook/guides/test-performance.g.md +353 -0
- data/handbook/guides/test-responsibility-map.g.md +220 -0
- data/handbook/guides/test-review-checklist.g.md +231 -0
- data/handbook/guides/test-suite-health.g.md +337 -0
- data/handbook/guides/testable-code-patterns.g.md +315 -0
- data/handbook/guides/testing/ruby-rspec-config-examples.md +120 -0
- data/handbook/guides/testing/ruby-rspec.md +87 -0
- data/handbook/guides/testing/rust.md +52 -0
- data/handbook/guides/testing/test-maintenance.md +364 -0
- data/handbook/guides/testing/typescript-bun.md +47 -0
- data/handbook/guides/testing/vue-firebase-auth.md +546 -0
- data/handbook/guides/testing/vue-vitest.md +236 -0
- data/handbook/guides/testing-philosophy.g.md +82 -0
- data/handbook/guides/testing-strategy.g.md +151 -0
- data/handbook/guides/testing-tdd-cycle.g.md +146 -0
- data/handbook/guides/testing.g.md +170 -0
- data/handbook/skills/as-test-create-cases/SKILL.md +24 -0
- data/handbook/skills/as-test-fix/SKILL.md +26 -0
- data/handbook/skills/as-test-improve-coverage/SKILL.md +22 -0
- data/handbook/skills/as-test-optimize/SKILL.md +34 -0
- data/handbook/skills/as-test-performance-audit/SKILL.md +34 -0
- data/handbook/skills/as-test-plan/SKILL.md +34 -0
- data/handbook/skills/as-test-review/SKILL.md +34 -0
- data/handbook/skills/as-test-verify-suite/SKILL.md +45 -0
- data/handbook/templates/e2e-sandbox-checklist.template.md +289 -0
- data/handbook/templates/test-case.template.md +56 -0
- data/handbook/templates/test-performance-audit.template.md +132 -0
- data/handbook/templates/test-responsibility-map.template.md +92 -0
- data/handbook/templates/test-review-checklist.template.md +163 -0
- data/handbook/workflow-instructions/test/analyze-failures.wf.md +120 -0
- data/handbook/workflow-instructions/test/create-cases.wf.md +675 -0
- data/handbook/workflow-instructions/test/fix.wf.md +120 -0
- data/handbook/workflow-instructions/test/improve-coverage.wf.md +370 -0
- data/handbook/workflow-instructions/test/optimize.wf.md +368 -0
- data/handbook/workflow-instructions/test/performance-audit.wf.md +17 -0
- data/handbook/workflow-instructions/test/plan.wf.md +323 -0
- data/handbook/workflow-instructions/test/review.wf.md +16 -0
- data/handbook/workflow-instructions/test/verify-suite.wf.md +343 -0
- data/lib/ace/test/version.rb +7 -0
- data/lib/ace/test.rb +10 -0
- metadata +152 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Test Layer Decision Guide
|
|
4
|
+
purpose: Help developers and agents decide where to test each behavior
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-02-23
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Layer Decision Guide
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
This guide helps you decide **where** to test each behavior. Placing tests at the wrong layer leads to slow feedback loops, brittle tests, or gaps in coverage.
|
|
15
|
+
|
|
16
|
+
## The Testing Pyramid
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
/\
|
|
20
|
+
/E2E\ 10% - Critical user journeys
|
|
21
|
+
/------\
|
|
22
|
+
/ Integ \ 20% - Component interactions
|
|
23
|
+
/----------\
|
|
24
|
+
/ Unit \ 70% - Pure logic, edge cases
|
|
25
|
+
/--------------\
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Layer | Target Time | What It Tests |
|
|
29
|
+
|-------|-------------|---------------|
|
|
30
|
+
| Unit (atoms) | <10ms | Pure functions, single responsibility |
|
|
31
|
+
| Unit (molecules) | <50ms | Composed operations, controlled I/O |
|
|
32
|
+
| Integration (organisms) | <500ms | Business logic orchestration |
|
|
33
|
+
| E2E | Seconds | Critical user workflows, real dependencies |
|
|
34
|
+
|
|
35
|
+
## Decision Matrix
|
|
36
|
+
|
|
37
|
+
Use this matrix to decide where a test belongs:
|
|
38
|
+
|
|
39
|
+
| Question | Unit | Integration | E2E |
|
|
40
|
+
|----------|:----:|:-----------:|:---:|
|
|
41
|
+
| Tests pure logic with no side effects? | **Yes** | - | - |
|
|
42
|
+
| Tests data transformation? | **Yes** | - | - |
|
|
43
|
+
| Tests component orchestration? | - | **Yes** | - |
|
|
44
|
+
| Needs real filesystem operations? | No | Sometimes | **Yes** |
|
|
45
|
+
| Needs real git repository? | No | Rarely | **Yes** |
|
|
46
|
+
| Needs real subprocess execution? | **Never** | Stub | **Yes** |
|
|
47
|
+
| Calls external APIs (GitHub, LLM)? | Mock | Mock | **Yes** |
|
|
48
|
+
| Tests CLI argument parsing? | API | API | **Yes** |
|
|
49
|
+
| Tests CLI output format? | - | 1 per file | **Yes** |
|
|
50
|
+
| Tests error messages and exit codes? | API | API | **Yes** |
|
|
51
|
+
| Tests tool installation/availability? | - | - | **Yes** |
|
|
52
|
+
| Tests multi-step user workflow? | - | - | **Yes** |
|
|
53
|
+
|
|
54
|
+
## Layer Responsibilities
|
|
55
|
+
|
|
56
|
+
### Unit Tests (atoms/molecules)
|
|
57
|
+
|
|
58
|
+
**Purpose**: Verify individual functions work correctly in isolation.
|
|
59
|
+
|
|
60
|
+
**Test these behaviors**:
|
|
61
|
+
- Pure function logic (input → output)
|
|
62
|
+
- Edge cases (empty input, nil, boundaries)
|
|
63
|
+
- Error handling (invalid input, exceptions)
|
|
64
|
+
- Data transformations
|
|
65
|
+
- Configuration parsing
|
|
66
|
+
- String/path manipulation
|
|
67
|
+
|
|
68
|
+
**Stub everything external**:
|
|
69
|
+
- Filesystem → use temp files or mocks
|
|
70
|
+
- Subprocess → stub `Open3.capture3`, `system()`
|
|
71
|
+
- Network → stub with WebMock
|
|
72
|
+
- Git → use `MockGitRepo`
|
|
73
|
+
- Time → stub `Time.now` if needed
|
|
74
|
+
|
|
75
|
+
**Example**:
|
|
76
|
+
```ruby
|
|
77
|
+
# atoms/path_expander_test.rb
|
|
78
|
+
def test_expands_home_directory
|
|
79
|
+
result = PathExpander.expand("~/config.yml")
|
|
80
|
+
assert_equal "/Users/test/config.yml", result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_returns_absolute_path_unchanged
|
|
84
|
+
result = PathExpander.expand("/absolute/path.yml")
|
|
85
|
+
assert_equal "/absolute/path.yml", result
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Integration Tests (molecules/organisms)
|
|
90
|
+
|
|
91
|
+
**Purpose**: Verify components work together correctly.
|
|
92
|
+
|
|
93
|
+
**Test these behaviors**:
|
|
94
|
+
- Data flow between modules
|
|
95
|
+
- Error propagation across components
|
|
96
|
+
- Configuration cascade resolution
|
|
97
|
+
- Orchestration logic
|
|
98
|
+
- ONE CLI parity test per file (verify CLI matches API)
|
|
99
|
+
|
|
100
|
+
**Stub external dependencies**:
|
|
101
|
+
- Real subprocess calls
|
|
102
|
+
- External APIs
|
|
103
|
+
- Slow operations (git init, network)
|
|
104
|
+
|
|
105
|
+
**Allow controlled I/O**:
|
|
106
|
+
- Temp directories for file tests
|
|
107
|
+
- In-memory data structures
|
|
108
|
+
|
|
109
|
+
**Example**:
|
|
110
|
+
```ruby
|
|
111
|
+
# organisms/config_resolver_test.rb
|
|
112
|
+
def test_merges_project_over_user_config
|
|
113
|
+
with_temp_config_files(
|
|
114
|
+
user: { model: "gpt-4" },
|
|
115
|
+
project: { model: "claude" }
|
|
116
|
+
) do
|
|
117
|
+
result = ConfigResolver.resolve("llm")
|
|
118
|
+
assert_equal "claude", result[:model]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# ONE CLI parity test
|
|
123
|
+
def test_cli_matches_api_output
|
|
124
|
+
api_result = Ace::MyTool.process("input.txt")
|
|
125
|
+
cli_output, status = Open3.capture3("ace-mytool", "input.txt")
|
|
126
|
+
|
|
127
|
+
assert status.success?
|
|
128
|
+
assert_equal api_result.output, cli_output.strip
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### E2E Tests (manual tests)
|
|
133
|
+
|
|
134
|
+
**Purpose**: Verify complete user workflows work in real environments.
|
|
135
|
+
|
|
136
|
+
**Test these behaviors**:
|
|
137
|
+
- Critical user journeys end-to-end
|
|
138
|
+
- Tool installation and availability
|
|
139
|
+
- Real API interactions (sandboxed)
|
|
140
|
+
- Complex multi-step workflows
|
|
141
|
+
- Environment-specific behavior
|
|
142
|
+
- CLI behavior with real tools
|
|
143
|
+
|
|
144
|
+
**Use real dependencies**:
|
|
145
|
+
- Real filesystem
|
|
146
|
+
- Real git operations
|
|
147
|
+
- Real subprocess calls
|
|
148
|
+
- Real external tools (StandardRB, gitleaks, etc.)
|
|
149
|
+
|
|
150
|
+
**Example** (TS-format `TC-*.tc.md`):
|
|
151
|
+
```markdown
|
|
152
|
+
### TC-001: Full Lint Workflow
|
|
153
|
+
|
|
154
|
+
**Steps:**
|
|
155
|
+
1. Create test file with lint issues
|
|
156
|
+
2. Run `ace-lint test.rb`
|
|
157
|
+
3. Verify issues detected
|
|
158
|
+
4. Run `ace-lint test.rb --fix`
|
|
159
|
+
5. Verify issues fixed
|
|
160
|
+
|
|
161
|
+
**Expected:**
|
|
162
|
+
- Step 2: Exit code 1, issues listed
|
|
163
|
+
- Step 4: Exit code 0, file modified
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Quick Reference: Where Does This Test Go?
|
|
167
|
+
|
|
168
|
+
### Put in Unit Tests
|
|
169
|
+
- "Does `parse_config` handle empty YAML?"
|
|
170
|
+
- "Does `format_output` escape special characters?"
|
|
171
|
+
- "Does `validate_input` reject nil?"
|
|
172
|
+
- "Does `calculate_score` handle edge cases?"
|
|
173
|
+
|
|
174
|
+
### Put in Integration Tests
|
|
175
|
+
- "Does the config cascade merge correctly?"
|
|
176
|
+
- "Does error in component A propagate to B?"
|
|
177
|
+
- "Does the CLI produce same output as API?" (ONE test)
|
|
178
|
+
- "Does the workflow orchestrator coordinate correctly?"
|
|
179
|
+
|
|
180
|
+
### Put in E2E Tests
|
|
181
|
+
- "Does the full lint workflow work with real StandardRB?"
|
|
182
|
+
- "Can users run `ace-git-commit` from any directory?"
|
|
183
|
+
- "Does tool detect when gitleaks is not installed?"
|
|
184
|
+
- "Does the complete review workflow produce valid reports?"
|
|
185
|
+
|
|
186
|
+
## Common Mistakes
|
|
187
|
+
|
|
188
|
+
### Mistake 1: E2E Tests for Flag Permutations
|
|
189
|
+
|
|
190
|
+
**Wrong**: 10 E2E tests for each CLI flag combination
|
|
191
|
+
```ruby
|
|
192
|
+
# Each takes 500ms+ due to subprocess
|
|
193
|
+
def test_verbose_flag; Open3.capture3(BIN, "--verbose"); end
|
|
194
|
+
def test_quiet_flag; Open3.capture3(BIN, "--quiet"); end
|
|
195
|
+
def test_debug_flag; Open3.capture3(BIN, "--debug"); end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Right**: 1 E2E test + unit tests for flags
|
|
199
|
+
```ruby
|
|
200
|
+
# E2E: verify CLI works
|
|
201
|
+
def test_cli_executes_successfully
|
|
202
|
+
_, status = Open3.capture3(BIN, "input.txt")
|
|
203
|
+
assert status.success?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Unit: test flag handling via API
|
|
207
|
+
def test_verbose_flag_enables_debug_output
|
|
208
|
+
result = MyTool.process("input.txt", verbose: true)
|
|
209
|
+
assert result.debug_output_enabled?
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Mistake 2: Real Git in Unit Tests
|
|
214
|
+
|
|
215
|
+
**Wrong**: Each test creates real git repo (~150ms)
|
|
216
|
+
```ruby
|
|
217
|
+
def setup
|
|
218
|
+
@repo = create_real_git_repo # SLOW
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Right**: Use MockGitRepo for unit tests
|
|
223
|
+
```ruby
|
|
224
|
+
def setup
|
|
225
|
+
@repo = MockGitRepo.new # FAST
|
|
226
|
+
@repo.add_commit("abc123", message: "test commit")
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Mistake 3: Missing Availability Stubs
|
|
231
|
+
|
|
232
|
+
**Wrong**: Stub `run` but not `available?`
|
|
233
|
+
```ruby
|
|
234
|
+
Runner.stub(:run, result) do
|
|
235
|
+
subject.lint(file) # Calls available?() → subprocess!
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Right**: Stub entire call chain
|
|
240
|
+
```ruby
|
|
241
|
+
Runner.stub(:available?, true) do
|
|
242
|
+
Runner.stub(:run, result) do
|
|
243
|
+
subject.lint(file) # Fast
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Performance Targets
|
|
249
|
+
|
|
250
|
+
| Test Type | Target | Hard Limit | Action if Exceeded |
|
|
251
|
+
|-----------|--------|------------|-------------------|
|
|
252
|
+
| Unit (atoms) | <10ms | 50ms | Check for subprocess leaks |
|
|
253
|
+
| Unit (molecules) | <50ms | 100ms | Check for unstubbed deps |
|
|
254
|
+
| Integration | <500ms | 1s | Move real calls to E2E |
|
|
255
|
+
| E2E | <5s | 30s | Split into smaller scenarios |
|
|
256
|
+
|
|
257
|
+
## See Also
|
|
258
|
+
|
|
259
|
+
- [Test Performance Guide](guide://test-performance) - Optimization techniques
|
|
260
|
+
- [Test Mocking Patterns](guide://test-mocking-patterns) - How to stub correctly
|
|
261
|
+
- [E2E Testing Guide](guide://e2e-testing) - E2E test conventions
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Test Mocking Patterns Guide
|
|
4
|
+
purpose: Ensure mocks actually test real behavior and stay in sync with production
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-02-01
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Mocking Patterns Guide
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
This guide ensures your mocks:
|
|
15
|
+
1. Test **behavior**, not implementation details
|
|
16
|
+
2. Stay in sync with real APIs (no drift)
|
|
17
|
+
3. Don't become "zombies" that test nothing
|
|
18
|
+
|
|
19
|
+
## Core Principle: Test Behavior, Not Implementation
|
|
20
|
+
|
|
21
|
+
### The Difference
|
|
22
|
+
|
|
23
|
+
**Implementation testing** (fragile):
|
|
24
|
+
- "Was method X called?"
|
|
25
|
+
- "Was it called with these exact arguments?"
|
|
26
|
+
- "Was it called exactly 3 times?"
|
|
27
|
+
|
|
28
|
+
**Behavior testing** (robust):
|
|
29
|
+
- "Given this input, is the output correct?"
|
|
30
|
+
- "Given this error condition, is the error message helpful?"
|
|
31
|
+
- "Does the system reach the correct final state?"
|
|
32
|
+
|
|
33
|
+
### Example
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# BAD: Tests implementation (breaks when refactored)
|
|
37
|
+
def test_processes_data
|
|
38
|
+
processor = Minitest::Mock.new
|
|
39
|
+
processor.expect :transform, "result", ["input"]
|
|
40
|
+
processor.expect :validate, true, ["result"]
|
|
41
|
+
|
|
42
|
+
subject = DataHandler.new(processor)
|
|
43
|
+
subject.handle("input")
|
|
44
|
+
|
|
45
|
+
processor.verify # "Were these methods called?"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# GOOD: Tests behavior (survives refactoring)
|
|
49
|
+
def test_processes_data
|
|
50
|
+
result = DataHandler.new.handle("input")
|
|
51
|
+
|
|
52
|
+
assert result.success?
|
|
53
|
+
assert_equal "expected_output", result.value
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Stub Hierarchy: Use the Simplest Double
|
|
58
|
+
|
|
59
|
+
From simplest to most complex:
|
|
60
|
+
|
|
61
|
+
| Type | Purpose | When to Use |
|
|
62
|
+
|------|---------|-------------|
|
|
63
|
+
| **Dummy** | Placeholder, never used | Parameter that won't be called |
|
|
64
|
+
| **Stub** | Returns canned values | Control return values |
|
|
65
|
+
| **Spy** | Records calls for inspection | Verify interactions happened |
|
|
66
|
+
| **Mock** | Verifies expectations | Protocol/contract testing |
|
|
67
|
+
| **Fake** | Working implementation | Complex behavior needed |
|
|
68
|
+
|
|
69
|
+
**Rule**: Use the simplest type that meets your needs.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# Dummy - just needs to exist
|
|
73
|
+
def test_with_unused_dependency
|
|
74
|
+
dummy_logger = Object.new
|
|
75
|
+
subject = Worker.new(logger: dummy_logger)
|
|
76
|
+
# logger never called in this test path
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Stub - control return value
|
|
80
|
+
def test_with_stubbed_api
|
|
81
|
+
api_result = { status: "ok", data: [1, 2, 3] }
|
|
82
|
+
ApiClient.stub :fetch, api_result do
|
|
83
|
+
result = subject.process
|
|
84
|
+
assert_equal [1, 2, 3], result.items
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Fake - real behavior, simplified
|
|
89
|
+
class FakeFileSystem
|
|
90
|
+
def initialize
|
|
91
|
+
@files = {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def write(path, content)
|
|
95
|
+
@files[path] = content
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read(path)
|
|
99
|
+
@files[path] or raise "File not found: #{path}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Zombie Mocks: Detection and Prevention
|
|
105
|
+
|
|
106
|
+
### What Are Zombie Mocks?
|
|
107
|
+
|
|
108
|
+
Mocks that stub methods **no longer called** by the implementation. Tests pass but don't test anything real.
|
|
109
|
+
|
|
110
|
+
### How They Happen
|
|
111
|
+
|
|
112
|
+
1. Code is refactored, method renamed or removed
|
|
113
|
+
2. Tests still stub old method name
|
|
114
|
+
3. Real code path executes (slowly or incorrectly)
|
|
115
|
+
4. Test passes anyway
|
|
116
|
+
|
|
117
|
+
### Case Study
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Original implementation
|
|
121
|
+
class ChangeDetector
|
|
122
|
+
def get_diff(files)
|
|
123
|
+
execute_git_command("git diff #{files.join(' ')}")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Test stubbed this:
|
|
128
|
+
ChangeDetector.stub :execute_git_command, "" do
|
|
129
|
+
result = detector.get_diff(files) # Fast, stubbed
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Later, implementation changed:
|
|
133
|
+
class ChangeDetector
|
|
134
|
+
def get_diff(files)
|
|
135
|
+
Ace::Git::DiffOrchestrator.generate(files: files) # New method!
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Test still stubs old method - ZOMBIE!
|
|
140
|
+
ChangeDetector.stub :execute_git_command, "" do
|
|
141
|
+
result = detector.get_diff(files) # Slow! Real DiffOrchestrator runs
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Detection
|
|
146
|
+
|
|
147
|
+
1. **Profile tests**: `ace-test --profile 10`
|
|
148
|
+
2. **Look for slow unit tests**: >100ms indicates zombie
|
|
149
|
+
3. **Try breaking the stub**: Change stub return value, test should fail
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Zombie detection test
|
|
153
|
+
def test_stub_is_actually_used
|
|
154
|
+
# If this stub is a zombie, changing return value won't affect test
|
|
155
|
+
Runner.stub(:run, "UNEXPECTED_VALUE_12345") do
|
|
156
|
+
result = subject.lint(file)
|
|
157
|
+
# If test passes without checking for UNEXPECTED_VALUE_12345,
|
|
158
|
+
# the stub might be a zombie
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Prevention
|
|
164
|
+
|
|
165
|
+
1. **Update stubs when refactoring**: Part of the refactoring checklist
|
|
166
|
+
2. **Use composite helpers**: Centralized, easier to maintain
|
|
167
|
+
3. **Profile regularly**: Weekly `ace-test --profile 20`
|
|
168
|
+
4. **Document stub targets**: Comments explaining what's stubbed and why
|
|
169
|
+
|
|
170
|
+
## Composite Helpers: Reducing Stub Complexity
|
|
171
|
+
|
|
172
|
+
### The Problem
|
|
173
|
+
|
|
174
|
+
Deep nesting makes tests hard to read and maintain:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
def test_complex_workflow
|
|
178
|
+
mock_config do
|
|
179
|
+
mock_git_status do
|
|
180
|
+
mock_diff_generator do
|
|
181
|
+
mock_api_client do
|
|
182
|
+
result = subject.execute
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### The Solution
|
|
191
|
+
|
|
192
|
+
Composite helpers that combine related stubs:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
def test_complex_workflow
|
|
196
|
+
with_mock_repo_context(branch: "feature", clean: true) do
|
|
197
|
+
result = subject.execute
|
|
198
|
+
assert result.success?
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# In test_helper.rb
|
|
203
|
+
def with_mock_repo_context(branch: "main", clean: true, diff: nil)
|
|
204
|
+
mock_branch = build_branch_info(name: branch)
|
|
205
|
+
mock_status = clean ? :clean : :dirty
|
|
206
|
+
mock_diff ||= Ace::Git::Models::DiffResult.empty
|
|
207
|
+
|
|
208
|
+
Ace::Git::Molecules::BranchInfo.stub :fetch, mock_branch do
|
|
209
|
+
Ace::Git::Atoms::StatusChecker.stub :clean?, clean do
|
|
210
|
+
Ace::Git::Organisms::DiffOrchestrator.stub :generate, mock_diff do
|
|
211
|
+
yield
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Design Principles
|
|
219
|
+
|
|
220
|
+
1. **Sensible defaults**: Most tests use standard values
|
|
221
|
+
2. **Keyword arguments**: Override only what matters
|
|
222
|
+
3. **Clear naming**: `with_mock_<context>` pattern
|
|
223
|
+
4. **Single responsibility**: One helper per "context"
|
|
224
|
+
|
|
225
|
+
### Existing Composite Helpers
|
|
226
|
+
|
|
227
|
+
| Package | Helper | Stubs |
|
|
228
|
+
|---------|--------|-------|
|
|
229
|
+
| ace-git | `with_mock_repo_load` | BranchInfo + StatusChecker + DiffOrchestrator |
|
|
230
|
+
| ace-git-secrets | `with_rewrite_test_mocks` | gitleaks + rewriter + working directory |
|
|
231
|
+
| ace-lint | `with_stubbed_validators` | ValidatorRegistry + Runner availability |
|
|
232
|
+
| ace-review | `stub_synthesizer_prompt_path` | ace-nav subprocess |
|
|
233
|
+
|
|
234
|
+
## Contract Testing: Keeping Mocks in Sync
|
|
235
|
+
|
|
236
|
+
### The Problem
|
|
237
|
+
|
|
238
|
+
Mock data can drift from real API responses:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Mock returns this:
|
|
242
|
+
{ "status" => "ok", "items" => [] }
|
|
243
|
+
|
|
244
|
+
# Real API returns this:
|
|
245
|
+
{ "status" => "success", "data" => { "items" => [] } }
|
|
246
|
+
|
|
247
|
+
# Test passes, production fails!
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Solution 1: Snapshot-Based Mocks
|
|
251
|
+
|
|
252
|
+
Capture real API responses and use them as mocks:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# 1. Record real response (one-time, manual)
|
|
256
|
+
# curl https://api.github.com/repos/owner/repo/pulls/123 > fixtures/pr_response.json
|
|
257
|
+
|
|
258
|
+
# 2. Use in tests
|
|
259
|
+
def mock_pr_response
|
|
260
|
+
JSON.parse(File.read("fixtures/pr_response.json"))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_fetches_pr_details
|
|
264
|
+
WebMock.stub_request(:get, /pulls\/123/)
|
|
265
|
+
.to_return(body: mock_pr_response.to_json)
|
|
266
|
+
|
|
267
|
+
result = PrFetcher.fetch(123)
|
|
268
|
+
assert_equal "open", result.state
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Solution 2: Schema Validation
|
|
273
|
+
|
|
274
|
+
Validate mock data against OpenAPI/JSON Schema:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# fixtures/schemas/github_pr.json defines the schema
|
|
278
|
+
|
|
279
|
+
def test_mock_matches_schema
|
|
280
|
+
schema = JSON.parse(File.read("fixtures/schemas/github_pr.json"))
|
|
281
|
+
mock = mock_pr_response
|
|
282
|
+
|
|
283
|
+
errors = JSON::Validator.validate(schema, mock)
|
|
284
|
+
assert_empty errors, "Mock doesn't match schema: #{errors}"
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Solution 3: Periodic Drift Check
|
|
289
|
+
|
|
290
|
+
Scheduled job that compares mocks to real responses:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# Run monthly or after API version updates
|
|
294
|
+
def test_mock_matches_live_api
|
|
295
|
+
skip "Run manually to check for API drift"
|
|
296
|
+
|
|
297
|
+
live_response = real_api_client.fetch_pr(TEST_PR_ID)
|
|
298
|
+
mock_response = mock_pr_response
|
|
299
|
+
|
|
300
|
+
# Compare structure (not exact values)
|
|
301
|
+
assert_same_keys live_response, mock_response
|
|
302
|
+
assert_same_types live_response, mock_response
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Stubbing Patterns by Dependency Type
|
|
307
|
+
|
|
308
|
+
### Subprocess Calls
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# Stub Open3.capture3
|
|
312
|
+
Open3.stub :capture3, ["output", "", mock_status] do
|
|
313
|
+
result = Runner.execute("command")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Stub system()
|
|
317
|
+
Kernel.stub :system, true do
|
|
318
|
+
Runner.check_availability
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Don't forget availability checks!
|
|
322
|
+
Runner.stub(:available?, true) do
|
|
323
|
+
# Now stub the actual execution
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### HTTP Requests
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# Using WebMock
|
|
331
|
+
WebMock.stub_request(:get, "https://api.example.com/data")
|
|
332
|
+
.to_return(body: { items: [] }.to_json, status: 200)
|
|
333
|
+
|
|
334
|
+
# Using VCR for recorded responses
|
|
335
|
+
VCR.use_cassette("api_response") do
|
|
336
|
+
result = ApiClient.fetch_data
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Filesystem
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Temp directory helper
|
|
344
|
+
def with_temp_dir
|
|
345
|
+
Dir.mktmpdir do |dir|
|
|
346
|
+
yield dir
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Fake filesystem for complex tests
|
|
351
|
+
def test_with_fake_fs
|
|
352
|
+
fake_fs = FakeFileSystem.new
|
|
353
|
+
fake_fs.write("config.yml", "key: value")
|
|
354
|
+
|
|
355
|
+
subject = ConfigLoader.new(filesystem: fake_fs)
|
|
356
|
+
result = subject.load("config.yml")
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Time
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# Stub Time.now
|
|
364
|
+
Time.stub :now, Time.new(2026, 1, 31, 12, 0, 0) do
|
|
365
|
+
result = subject.generate_timestamp
|
|
366
|
+
assert_equal "2026-01-31T12:00:00", result
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Stub sleep for retry tests
|
|
370
|
+
Kernel.stub :sleep, nil do
|
|
371
|
+
result = subject.retry_with_backoff(max_retries: 3)
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Git Operations
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
# Use MockGitRepo (fast, no subprocess)
|
|
379
|
+
def test_with_mock_repo
|
|
380
|
+
repo = MockGitRepo.new
|
|
381
|
+
repo.add_commit("abc123", message: "Initial commit", files: ["README.md"])
|
|
382
|
+
repo.add_commit("def456", message: "Add feature", files: ["feature.rb"])
|
|
383
|
+
|
|
384
|
+
subject = CommitAnalyzer.new(repo: repo)
|
|
385
|
+
result = subject.analyze("def456")
|
|
386
|
+
|
|
387
|
+
assert_equal ["feature.rb"], result.changed_files
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Real git only in integration/E2E
|
|
391
|
+
def test_with_real_repo
|
|
392
|
+
with_temp_git_repo do |repo_path|
|
|
393
|
+
# Creates real .git directory
|
|
394
|
+
File.write("#{repo_path}/test.txt", "content")
|
|
395
|
+
system("git", "-C", repo_path, "add", ".")
|
|
396
|
+
system("git", "-C", repo_path, "commit", "-m", "test")
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Checklist: Is My Mock Testing Real Behavior?
|
|
402
|
+
|
|
403
|
+
- [ ] **Behavior focus**: Test checks output/state, not method calls
|
|
404
|
+
- [ ] **Stub is used**: Changing stub return value causes test to fail
|
|
405
|
+
- [ ] **Data is realistic**: Mock data from real API snapshot or validated schema
|
|
406
|
+
- [ ] **Complete chain**: All entry points to expensive operations stubbed
|
|
407
|
+
- [ ] **No zombie**: Stub target matches current implementation
|
|
408
|
+
- [ ] **Documented**: Comment explains what's stubbed and why
|
|
409
|
+
|
|
410
|
+
## See Also
|
|
411
|
+
|
|
412
|
+
- [Test Layer Decision](guide://test-layer-decision) - Where to test each behavior
|
|
413
|
+
- [Test Performance](guide://test-performance) - Performance optimization
|
|
414
|
+
- [E2E Testing](guide://e2e-testing) - When mocks aren't enough
|