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,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Test Organization
|
|
4
|
+
purpose: Test file organization
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-01-23
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Organization
|
|
11
|
+
|
|
12
|
+
## Flat Directory Structure
|
|
13
|
+
|
|
14
|
+
All ACE gems use a **flat test directory structure** that mirrors the ATOM architecture:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
test/
|
|
18
|
+
├── test_helper.rb
|
|
19
|
+
├── search_test.rb # Main module test
|
|
20
|
+
├── atoms/
|
|
21
|
+
│ ├── pattern_analyzer_test.rb
|
|
22
|
+
│ ├── result_parser_test.rb
|
|
23
|
+
│ └── tool_checker_test.rb
|
|
24
|
+
├── molecules/
|
|
25
|
+
│ ├── preset_manager_test.rb
|
|
26
|
+
│ └── git_scope_filter_test.rb
|
|
27
|
+
├── organisms/
|
|
28
|
+
│ ├── unified_searcher_test.rb
|
|
29
|
+
│ └── result_formatter_test.rb
|
|
30
|
+
├── models/
|
|
31
|
+
│ └── search_result_test.rb
|
|
32
|
+
└── integration/
|
|
33
|
+
└── cli_integration_test.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Key Conventions
|
|
37
|
+
|
|
38
|
+
- **Flat structure**: `test/atoms/`, not `test/ace/search/atoms/`
|
|
39
|
+
- **Suffix naming**: `pattern_analyzer_test.rb`, not `test_pattern_analyzer.rb`
|
|
40
|
+
- **Layer directories match ATOM architecture**: atoms, molecules, organisms
|
|
41
|
+
- **Integration tests in separate `integration/` directory**
|
|
42
|
+
|
|
43
|
+
## Benefits
|
|
44
|
+
|
|
45
|
+
- Easier to navigate and find tests
|
|
46
|
+
- Matches layer boundaries clearly
|
|
47
|
+
- Consistent across all ACE gems
|
|
48
|
+
- Less nesting = simpler paths
|
|
49
|
+
|
|
50
|
+
See `ace-taskflow/test/` for reference implementation.
|
|
51
|
+
|
|
52
|
+
## Naming Conventions
|
|
53
|
+
|
|
54
|
+
### Test Files
|
|
55
|
+
|
|
56
|
+
- Use `*_test.rb` suffix (Minitest convention)
|
|
57
|
+
- Name matches the class being tested: `PatternAnalyzer` → `pattern_analyzer_test.rb`
|
|
58
|
+
- One test file per class/module
|
|
59
|
+
|
|
60
|
+
### Test Methods
|
|
61
|
+
|
|
62
|
+
- Use `test_` prefix: `def test_finds_patterns_in_code`
|
|
63
|
+
- Be descriptive: `test_returns_empty_when_no_matches` not `test_empty`
|
|
64
|
+
- Include the scenario: `test_raises_error_on_invalid_input`
|
|
65
|
+
|
|
66
|
+
### Test Classes
|
|
67
|
+
|
|
68
|
+
- Mirror the class hierarchy: `class PatternAnalyzerTest < Minitest::Test`
|
|
69
|
+
- Group related tests with modules if needed
|
|
70
|
+
|
|
71
|
+
## Test Data
|
|
72
|
+
|
|
73
|
+
### Fixtures
|
|
74
|
+
|
|
75
|
+
Store test data in `test/fixtures/`:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
test/fixtures/
|
|
79
|
+
├── sample_config.yml
|
|
80
|
+
├── git_diff_output.txt
|
|
81
|
+
└── api_responses/
|
|
82
|
+
└── github_pr_123.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Creating Fixtures
|
|
86
|
+
|
|
87
|
+
Use `yaml_fixture` helper for YAML fixtures:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
def test_loads_config
|
|
91
|
+
config = yaml_fixture("sample_config.yml")
|
|
92
|
+
assert_equal "expected_value", config["key"]
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Inline Data
|
|
97
|
+
|
|
98
|
+
Prefer inline data for small test cases:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
def test_parses_simple_input
|
|
102
|
+
input = "key: value"
|
|
103
|
+
result = Parser.parse(input)
|
|
104
|
+
assert_equal "value", result["key"]
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Test Helpers
|
|
109
|
+
|
|
110
|
+
### Location
|
|
111
|
+
|
|
112
|
+
Place shared helpers in `test/test_helper.rb` or a dedicated `test/support/` directory:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
test/
|
|
116
|
+
├── test_helper.rb
|
|
117
|
+
└── support/
|
|
118
|
+
├── mock_git_repo.rb
|
|
119
|
+
└── api_stubs.rb
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Including Helpers
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# test_helper.rb
|
|
126
|
+
require_relative "support/mock_git_repo"
|
|
127
|
+
|
|
128
|
+
module TestHelpers
|
|
129
|
+
include MockGitRepo
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class Minitest::Test
|
|
133
|
+
include TestHelpers
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Related Guides
|
|
138
|
+
|
|
139
|
+
- [Testing Philosophy](guide://testing-philosophy) - Why this structure
|
|
140
|
+
- [Mocking Patterns](guide://mocking-patterns) - Test isolation patterns
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Test Performance
|
|
4
|
+
purpose: Test performance optimization
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-01-23
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Performance
|
|
11
|
+
|
|
12
|
+
## Performance Targets
|
|
13
|
+
|
|
14
|
+
Define explicit performance expectations for each test layer based on patterns established during optimization of 9 ACE packages (60% average improvement).
|
|
15
|
+
|
|
16
|
+
### Performance Thresholds by Test Type
|
|
17
|
+
|
|
18
|
+
| Test Layer | Target Time | Hard Limit | Common Issues |
|
|
19
|
+
|------------|-------------|------------|---------------|
|
|
20
|
+
| Unit (atoms) | <10ms | 50ms | Real git ops, subprocess spawns |
|
|
21
|
+
| Unit (molecules) | <50ms | 100ms | Unstubbed dependencies |
|
|
22
|
+
| Unit (organisms) | <100ms | 200ms | Missing composite helpers |
|
|
23
|
+
| Integration | <500ms | 1s | Too many real operations |
|
|
24
|
+
| E2E | <2s | 5s | Should be rare - ONE per file |
|
|
25
|
+
|
|
26
|
+
### Performance Cost Reference
|
|
27
|
+
|
|
28
|
+
Know the cost of common operations to guide optimization:
|
|
29
|
+
|
|
30
|
+
| Operation | Typical Cost | Notes |
|
|
31
|
+
|-----------|--------------|-------|
|
|
32
|
+
| Real `git init` | ~150-200ms | Use MockGitRepo instead |
|
|
33
|
+
| Real `git commit` | ~50-100ms | Stub in unit tests |
|
|
34
|
+
| Subprocess spawn (`Open3.capture3`) | ~150ms | Stub or use API calls |
|
|
35
|
+
| Sleep in retry tests | 1-2s per sleep | Stub `Kernel.sleep` |
|
|
36
|
+
| Cross-package require (cold) | ~50-100ms | Cache or stub dependencies |
|
|
37
|
+
| `ace-nav` subprocess | ~150-400ms | Use `stub_synthesizer_prompt_path` |
|
|
38
|
+
| Real LLM API call | 1-20s | Use WebMock to stub HTTP |
|
|
39
|
+
| Real GitHub API call | 100-500ms | Use WebMock to stub HTTP |
|
|
40
|
+
|
|
41
|
+
### When Tests Exceed Targets
|
|
42
|
+
|
|
43
|
+
1. **Profile first**: Run `ace-test --profile 10` to identify actual bottlenecks
|
|
44
|
+
2. **Check for zombie mocks**: Stubs that don't match actual code paths (see Zombie Mocks section)
|
|
45
|
+
3. **Verify stubbing layer**: Stub at the boundary closest to your test subject
|
|
46
|
+
4. **Consider composite helpers**: Reduce setup overhead with consolidated mock helpers
|
|
47
|
+
5. **Apply E2E rule**: Keep ONE E2E test per file, convert rest to mocked versions
|
|
48
|
+
|
|
49
|
+
## Sleep Stubbing for Retry Tests
|
|
50
|
+
|
|
51
|
+
Tests with retry logic often include `sleep` calls that add seconds to test runtime.
|
|
52
|
+
|
|
53
|
+
### Pattern: Stub Kernel.sleep
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
def with_stubbed_sleep
|
|
57
|
+
Kernel.stub :sleep, nil do
|
|
58
|
+
yield
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_retry_logic
|
|
63
|
+
with_stubbed_sleep do
|
|
64
|
+
# Retry tests now instant instead of 3+ seconds
|
|
65
|
+
result = RetryableOperation.call(max_retries: 3, delay: 1.0)
|
|
66
|
+
assert result.eventually_succeeded?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Alternative: Inject Sleep Dependency
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Production code
|
|
75
|
+
class RetryableOperation
|
|
76
|
+
def initialize(sleeper: Kernel)
|
|
77
|
+
@sleeper = sleeper
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def call
|
|
81
|
+
attempts = 0
|
|
82
|
+
loop do
|
|
83
|
+
result = try_operation
|
|
84
|
+
return result if result.success?
|
|
85
|
+
attempts += 1
|
|
86
|
+
break if attempts >= max_retries
|
|
87
|
+
@sleeper.sleep(delay)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Test with null sleeper
|
|
93
|
+
def test_retry_without_delay
|
|
94
|
+
null_sleeper = Object.new
|
|
95
|
+
null_sleeper.define_singleton_method(:sleep) { |_| nil }
|
|
96
|
+
|
|
97
|
+
op = RetryableOperation.new(sleeper: null_sleeper)
|
|
98
|
+
result = op.call
|
|
99
|
+
assert result.eventually_succeeded?
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## E2E Test Strategy: Keep ONE Per Integration File
|
|
104
|
+
|
|
105
|
+
Keep exactly ONE E2E test per integration test file that exercises real subprocess calls. Convert all other tests to use mocked versions.
|
|
106
|
+
|
|
107
|
+
### When to Use Real E2E Tests
|
|
108
|
+
|
|
109
|
+
**Keep as E2E (real subprocess)**:
|
|
110
|
+
- CLI parity validation (CLI vs API produce same result)
|
|
111
|
+
- Critical path smoke tests
|
|
112
|
+
- Tool availability checks (gitleaks, git-filter-repo)
|
|
113
|
+
- One representative test per integration file
|
|
114
|
+
|
|
115
|
+
**Convert to Mocked**:
|
|
116
|
+
- Flag/option permutation tests
|
|
117
|
+
- Error handling tests
|
|
118
|
+
- Edge case tests
|
|
119
|
+
- Performance-critical paths
|
|
120
|
+
|
|
121
|
+
### Migration Pattern: E2E to Mocked
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# BEFORE: E2E test using subprocess (~500ms each, 5 tests = 2.5s)
|
|
125
|
+
def test_cli_with_verbose_flag
|
|
126
|
+
output, status = Open3.capture3(BIN, "analyze", "--verbose")
|
|
127
|
+
assert status.success?
|
|
128
|
+
assert_includes output, "Verbose output"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_cli_with_quiet_flag
|
|
132
|
+
output, status = Open3.capture3(BIN, "analyze", "--quiet")
|
|
133
|
+
assert status.success?
|
|
134
|
+
refute_includes output, "Debug"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# AFTER: Keep ONE E2E, convert rest to API tests (~5ms each)
|
|
138
|
+
def test_cli_parity_with_api # Keep this ONE E2E test
|
|
139
|
+
cli_output, status = Open3.capture3(BIN, "analyze", "file.rb")
|
|
140
|
+
api_result = Ace::MyModule.analyze("file.rb")
|
|
141
|
+
assert status.success?
|
|
142
|
+
assert_equal api_result.output, cli_output.strip
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_verbose_flag # Converted to API test
|
|
146
|
+
result = Ace::MyModule.analyze("file.rb", verbose: true)
|
|
147
|
+
assert result.success?
|
|
148
|
+
assert_includes result.output, "Verbose output"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_quiet_flag # Converted to API test
|
|
152
|
+
result = Ace::MyModule.analyze("file.rb", quiet: true)
|
|
153
|
+
assert result.success?
|
|
154
|
+
refute_includes result.output, "Debug"
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Real Example: ace-test-runner Optimization
|
|
159
|
+
|
|
160
|
+
Task 175 reduced ace-test-runner tests from 8.25s to 3.3s (60% reduction) by:
|
|
161
|
+
1. Keeping ONE E2E test for genuine CLI validation
|
|
162
|
+
2. Converting 2 redundant E2E tests to use `run_ace_test_with_mock` helper
|
|
163
|
+
3. Adding TestRunnerMocks infrastructure to ace-support-test-helpers
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# Helper for mocked CLI tests
|
|
167
|
+
def run_ace_test_with_mock(args, expected_output: "", expected_status: 0)
|
|
168
|
+
mock_status = Object.new
|
|
169
|
+
mock_status.define_singleton_method(:success?) { expected_status == 0 }
|
|
170
|
+
mock_status.define_singleton_method(:exitstatus) { expected_status }
|
|
171
|
+
|
|
172
|
+
Open3.stub :capture3, [expected_output, "", mock_status] do
|
|
173
|
+
yield
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Composite Test Helpers
|
|
179
|
+
|
|
180
|
+
Reduce deeply nested stubs by creating composite helpers that combine related mocks.
|
|
181
|
+
|
|
182
|
+
### The Problem: Deep Nesting
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# BAD: 6-7 levels of nesting (hard to read, slow due to setup overhead)
|
|
186
|
+
def test_complex_operation
|
|
187
|
+
mock_config_loader do
|
|
188
|
+
mock_diff_generator do
|
|
189
|
+
mock_diff_filter do
|
|
190
|
+
mock_branch_info do
|
|
191
|
+
mock_pr_fetcher do
|
|
192
|
+
mock_commits_fetcher do
|
|
193
|
+
result = SUT.call
|
|
194
|
+
assert result.success?
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### The Solution: Composite Helpers
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# GOOD: Single composite helper with keyword options
|
|
208
|
+
def test_complex_operation
|
|
209
|
+
with_mock_repo_load(branch: "feature", task_pattern: "123") do
|
|
210
|
+
result = SUT.call
|
|
211
|
+
assert result.success?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# In test_helper.rb - consolidates 6 stubs into one helper
|
|
216
|
+
def with_mock_repo_load(branch: "main", task_pattern: nil, usable: true)
|
|
217
|
+
branch_info = build_mock_branch_info(name: branch, task_pattern: task_pattern)
|
|
218
|
+
mock_config = build_mock_config
|
|
219
|
+
mock_diff = Ace::Git::Models::DiffResult.empty
|
|
220
|
+
|
|
221
|
+
Ace::Config.stub :create, mock_config do
|
|
222
|
+
Ace::Git::Molecules::BranchInfo.stub :fetch, branch_info do
|
|
223
|
+
Ace::Git::Organisms::DiffOrchestrator.stub :generate, mock_diff do
|
|
224
|
+
yield
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Composite Helper Design Principles
|
|
232
|
+
|
|
233
|
+
1. **Sensible Defaults**: Most tests need standard values; customize only what matters
|
|
234
|
+
2. **Keyword Arguments**: Allow targeted overrides without changing unrelated values
|
|
235
|
+
3. **Clear Naming**: `with_mock_<context>` pattern indicates scope
|
|
236
|
+
4. **Single Responsibility**: Each helper handles one "thing" completely
|
|
237
|
+
|
|
238
|
+
### Examples from ACE Packages
|
|
239
|
+
|
|
240
|
+
| Package | Helper | Purpose |
|
|
241
|
+
|---------|--------|---------|
|
|
242
|
+
| ace-git | `with_mock_repo_load` | Combines 6 stubs for RepoStatusLoader |
|
|
243
|
+
| ace-git | `with_mock_diff_orchestrator` | ConfigLoader + DiffGenerator + DiffFilter |
|
|
244
|
+
| ace-git-secrets | `with_rewrite_test_mocks` | gitleaks + rewriter + working directory |
|
|
245
|
+
| ace-taskflow | `with_real_test_project` | ConfigResolver + project setup |
|
|
246
|
+
| ace-docs | `with_empty_git_diff` | Simple DiffOrchestrator stub |
|
|
247
|
+
| ace-review | `stub_synthesizer_prompt_path` | ace-nav subprocess stub |
|
|
248
|
+
|
|
249
|
+
### Implementation Pattern
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# In test_helper.rb
|
|
253
|
+
module CompositeHelpers
|
|
254
|
+
def with_empty_git_diff
|
|
255
|
+
empty_result = Ace::Git::Models::DiffResult.empty
|
|
256
|
+
Ace::Git::Organisms::DiffOrchestrator.stub(:generate, empty_result) do
|
|
257
|
+
yield
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def with_mock_diff(content:, files: [])
|
|
262
|
+
mock_result = Ace::Git::Models::DiffResult.new(
|
|
263
|
+
content: content,
|
|
264
|
+
stats: { additions: 1, deletions: 0, files: files.size },
|
|
265
|
+
files: files
|
|
266
|
+
)
|
|
267
|
+
Ace::Git::Organisms::DiffOrchestrator.stub(:generate, mock_result) do
|
|
268
|
+
yield
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def build_mock_status(success: true, exitstatus: 0)
|
|
273
|
+
status = Object.new
|
|
274
|
+
status.define_singleton_method(:success?) { success }
|
|
275
|
+
status.define_singleton_method(:exitstatus) { exitstatus }
|
|
276
|
+
status
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
class MyTestCase < Minitest::Test
|
|
281
|
+
include CompositeHelpers
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Zombie Mocks Pattern
|
|
286
|
+
|
|
287
|
+
"Zombie Mocks" occur when mocks stub methods that are no longer called by the implementation, but tests continue to pass because the real code path happens to work (slowly or otherwise).
|
|
288
|
+
|
|
289
|
+
### Symptoms
|
|
290
|
+
|
|
291
|
+
- Tests pass but are unexpectedly slow
|
|
292
|
+
- Mock setup doesn't match actual code implementation
|
|
293
|
+
- Refactored code still uses old mock patterns
|
|
294
|
+
|
|
295
|
+
### Case Study: ace-docs ChangeDetector
|
|
296
|
+
|
|
297
|
+
**Problem**: Tests stubbed `ChangeDetector.stub :execute_git_command` but the implementation had evolved to use `Ace::Git::Organisms::DiffOrchestrator.generate`. Tests passed but each ran real git operations (~1 second each).
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
# ZOMBIE MOCK - stubs method no longer in code path
|
|
301
|
+
ChangeDetector.stub :execute_git_command, "" do
|
|
302
|
+
result = ChangeDetector.get_diff_for_documents(docs, since: "HEAD~1")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# CORRECT - stubs actual method being called
|
|
306
|
+
mock_result = Ace::Git::Models::DiffResult.empty
|
|
307
|
+
Ace::Git::Organisms::DiffOrchestrator.stub :generate, mock_result do
|
|
308
|
+
result = ChangeDetector.get_diff_for_documents(docs, since: "HEAD~1")
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Detection**: Run `ace-test --profile 10` to find slow unit tests. Tests taking >100ms often indicate zombie mocks.
|
|
313
|
+
|
|
314
|
+
**Result**: Fixing zombie mocks reduced test time from 14s to 1.5s (89% improvement).
|
|
315
|
+
|
|
316
|
+
### Prevention
|
|
317
|
+
|
|
318
|
+
1. **Profile regularly**: Add `ace-test --profile 10` to development workflow
|
|
319
|
+
2. **Review mock targets**: When refactoring, update test mocks to match new code paths
|
|
320
|
+
3. **Extract helpers**: Create reusable mock helpers (like `with_empty_git_diff`) that are easy to maintain
|
|
321
|
+
4. **Test the mocks**: Verify mocks are being hit by temporarily breaking them
|
|
322
|
+
|
|
323
|
+
## When to Investigate Test Performance
|
|
324
|
+
|
|
325
|
+
1. Run tests with profiling: `ace-test --profile 20`
|
|
326
|
+
2. Look for patterns in slow tests (similar names, same file)
|
|
327
|
+
3. Check for:
|
|
328
|
+
- Subprocess spawning
|
|
329
|
+
- Network I/O
|
|
330
|
+
- Disk I/O
|
|
331
|
+
- Sleep statements
|
|
332
|
+
- Large data processing
|
|
333
|
+
|
|
334
|
+
## Monitoring Test Performance
|
|
335
|
+
|
|
336
|
+
Add to your CI pipeline:
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
- name: Check test performance
|
|
340
|
+
run: |
|
|
341
|
+
ace-test --profile 20 | tee profile.txt
|
|
342
|
+
# Fail if any test takes >100ms (except integration tests)
|
|
343
|
+
if grep -E "^\s+[0-9]+\.\s+test_(?!integration)" profile.txt | awk '{print $NF}' | grep -E "[0-9]+\.[1-9][0-9][0-9]s"; then
|
|
344
|
+
echo "Tests taking >100ms detected"
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Related Guides
|
|
350
|
+
|
|
351
|
+
- [Mocking Patterns](guide://mocking-patterns) - How to stub properly
|
|
352
|
+
- [Testing Philosophy](guide://testing-philosophy) - Why performance matters
|
|
353
|
+
- [Testable Code Patterns](guide://testable-code-patterns) - Designing for fast tests
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Test Responsibility Map Guide
|
|
4
|
+
purpose: Test responsibility mapping and risk-based coverage
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-02-22
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Responsibility Map Guide
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
A Test Responsibility Map assigns each behavior to the **lowest test layer** that can prove it. This:
|
|
15
|
+
- Prevents duplicate testing across layers
|
|
16
|
+
- Keeps the fast loop fast
|
|
17
|
+
- Ensures critical workflows get E2E coverage
|
|
18
|
+
- Makes coverage gaps visible
|
|
19
|
+
|
|
20
|
+
## Core Principle
|
|
21
|
+
|
|
22
|
+
> Each behavior belongs to exactly ONE test layer. Test it at the lowest layer possible, promote only when necessary.
|
|
23
|
+
|
|
24
|
+
## The Mapping Process
|
|
25
|
+
|
|
26
|
+
### Step 1: List Behaviors
|
|
27
|
+
|
|
28
|
+
Identify all behaviors from requirements:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## Behaviors for ConfigParser
|
|
32
|
+
|
|
33
|
+
1. Parse valid YAML file
|
|
34
|
+
2. Return defaults for missing keys
|
|
35
|
+
3. Raise error for malformed YAML
|
|
36
|
+
4. Handle empty file
|
|
37
|
+
5. Merge cascading configs
|
|
38
|
+
6. CLI reports config errors with exit code 1
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Step 2: Assign Risk Levels
|
|
42
|
+
|
|
43
|
+
| Risk Level | Criteria | Testing Intensity |
|
|
44
|
+
|------------|----------|-------------------|
|
|
45
|
+
| **High** | Security, data integrity, core business, user-facing errors | Must have unit + E2E |
|
|
46
|
+
| **Medium** | Important functionality, configuration, integrations | Unit required |
|
|
47
|
+
| **Low** | Logging, cosmetic, internal helpers | Unit if time permits |
|
|
48
|
+
|
|
49
|
+
### Step 3: Map to Layers
|
|
50
|
+
|
|
51
|
+
For each behavior, ask:
|
|
52
|
+
|
|
53
|
+
1. **Can a unit test prove this?** (No I/O needed) → Unit
|
|
54
|
+
2. **Does it need component interaction?** (Stubbed I/O) → Integration
|
|
55
|
+
3. **Does it require real I/O to prove?** (CLI, network, filesystem) → E2E
|
|
56
|
+
|
|
57
|
+
### Step 4: Build the Map
|
|
58
|
+
|
|
59
|
+
| Behavior | Risk | Layer | Test File | Source of Truth |
|
|
60
|
+
|----------|------|-------|-----------|-----------------|
|
|
61
|
+
| Parse valid YAML | Medium | Unit | config_parser_test.rb | YAML schema |
|
|
62
|
+
| Return defaults | Medium | Unit | config_parser_test.rb | defaults.yml |
|
|
63
|
+
| Malformed YAML error | High | Unit | config_parser_test.rb | Exception spec |
|
|
64
|
+
| Config cascade merge | Medium | Integration | config_resolver_test.rb | Merge rules |
|
|
65
|
+
| CLI exit code 1 | High | E2E | TS-CONFIG-001 | CLI spec |
|
|
66
|
+
|
|
67
|
+
## Layer Decision Rules
|
|
68
|
+
|
|
69
|
+
### Unit Test If:
|
|
70
|
+
|
|
71
|
+
- Pure logic with no side effects
|
|
72
|
+
- Data transformation or validation
|
|
73
|
+
- Error handling for invalid input
|
|
74
|
+
- Edge cases (nil, empty, boundaries)
|
|
75
|
+
|
|
76
|
+
**Stub everything**: filesystem, network, subprocess, git
|
|
77
|
+
|
|
78
|
+
### Integration Test If:
|
|
79
|
+
|
|
80
|
+
- Multiple components interact
|
|
81
|
+
- Data flows between modules
|
|
82
|
+
- Error propagation matters
|
|
83
|
+
- ONE CLI parity check needed
|
|
84
|
+
|
|
85
|
+
**Stub external dependencies**: APIs, subprocess calls
|
|
86
|
+
|
|
87
|
+
### E2E Test If:
|
|
88
|
+
|
|
89
|
+
- Complete user workflow
|
|
90
|
+
- Real tool interaction required
|
|
91
|
+
- Environment-specific behavior
|
|
92
|
+
- Cannot be proven without real I/O
|
|
93
|
+
|
|
94
|
+
**Use real I/O**: sandboxed, with cleanup
|
|
95
|
+
|
|
96
|
+
## Avoiding Redundancy
|
|
97
|
+
|
|
98
|
+
### Anti-Pattern: Testing Same Behavior at Multiple Layers
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
# BAD: Same behavior tested 3 times
|
|
102
|
+
- Unit: test_config_parser_returns_defaults
|
|
103
|
+
- Integration: test_config_loader_uses_defaults
|
|
104
|
+
- E2E: TC-001 verifies defaults in CLI output
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pattern: Test at Lowest Layer, Verify at Higher
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
# GOOD: Behavior at lowest, workflow at highest
|
|
111
|
+
- Unit: test_config_parser_returns_defaults (proves logic)
|
|
112
|
+
- Integration: (skip - unit covers it)
|
|
113
|
+
- E2E: TC-001 verifies full config workflow (one test, not per feature)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Risk-Based Coverage
|
|
117
|
+
|
|
118
|
+
### High Risk Behaviors
|
|
119
|
+
|
|
120
|
+
Must be tested thoroughly:
|
|
121
|
+
|
|
122
|
+
```markdown
|
|
123
|
+
| Behavior | Risk | Why High | Coverage |
|
|
124
|
+
|----------|------|----------|----------|
|
|
125
|
+
| Auth token validation | High | Security | Unit + E2E |
|
|
126
|
+
| Data persistence | High | Data integrity | Unit + Integration + E2E |
|
|
127
|
+
| Payment processing | High | Business critical | Unit + Integration + E2E |
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Low Risk Behaviors
|
|
131
|
+
|
|
132
|
+
Basic coverage sufficient:
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
| Behavior | Risk | Why Low | Coverage |
|
|
136
|
+
|----------|------|---------|----------|
|
|
137
|
+
| Log formatting | Low | Cosmetic | Unit happy path |
|
|
138
|
+
| Debug output | Low | Internal | Skip or minimal |
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Template Usage
|
|
142
|
+
|
|
143
|
+
Use the template at `templates/test-responsibility-map.template.md` when:
|
|
144
|
+
|
|
145
|
+
- Starting a new feature
|
|
146
|
+
- Auditing existing coverage
|
|
147
|
+
- Planning test refactoring
|
|
148
|
+
- Reviewing PR test coverage
|
|
149
|
+
|
|
150
|
+
## Review Questions
|
|
151
|
+
|
|
152
|
+
When reviewing a responsibility map:
|
|
153
|
+
|
|
154
|
+
- [ ] Is each behavior at the lowest possible layer?
|
|
155
|
+
- [ ] Are high-risk behaviors covered by E2E?
|
|
156
|
+
- [ ] Are edge cases in unit tests, not E2E?
|
|
157
|
+
- [ ] Any duplicate coverage across layers?
|
|
158
|
+
- [ ] Source of truth identified for each behavior?
|
|
159
|
+
|
|
160
|
+
## Common Mistakes
|
|
161
|
+
|
|
162
|
+
### Mistake 1: E2E for Edge Cases
|
|
163
|
+
|
|
164
|
+
```markdown
|
|
165
|
+
# BAD: Testing every edge case in E2E
|
|
166
|
+
- E2E: TC-001 valid config
|
|
167
|
+
- E2E: TC-002 empty config
|
|
168
|
+
- E2E: TC-003 missing key
|
|
169
|
+
- E2E: TC-004 invalid YAML
|
|
170
|
+
- E2E: TC-005 circular reference
|
|
171
|
+
|
|
172
|
+
# GOOD: Edge cases in unit, workflow in E2E
|
|
173
|
+
- Unit: test_empty_config, test_missing_key, test_invalid_yaml, test_circular_ref
|
|
174
|
+
- E2E: TC-001 complete config workflow (happy + one error)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Mistake 2: Missing High-Risk E2E
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
# BAD: High-risk behavior only unit tested
|
|
181
|
+
- Unit: test_auth_token_validation ✓
|
|
182
|
+
- E2E: (none)
|
|
183
|
+
|
|
184
|
+
# GOOD: High-risk has E2E verification
|
|
185
|
+
- Unit: test_auth_token_validation ✓
|
|
186
|
+
- E2E: TC-001 authentication workflow ✓
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Mistake 3: No Source of Truth
|
|
190
|
+
|
|
191
|
+
```markdown
|
|
192
|
+
# BAD: Mock data invented
|
|
193
|
+
mock_response = { status: "ok" } # Where does this come from?
|
|
194
|
+
|
|
195
|
+
# GOOD: Mock data from real source
|
|
196
|
+
mock_response = JSON.parse(File.read("fixtures/api_response.json"))
|
|
197
|
+
# fixtures/api_response.json is snapshot from real API
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Integration with Workflows
|
|
201
|
+
|
|
202
|
+
### With /ace-test-plan
|
|
203
|
+
|
|
204
|
+
1. Generate responsibility map
|
|
205
|
+
2. Identify gaps
|
|
206
|
+
3. Plan tests by layer
|
|
207
|
+
4. Output test plan
|
|
208
|
+
|
|
209
|
+
### With /ace-test-verify-suite
|
|
210
|
+
|
|
211
|
+
1. Check existing tests against map
|
|
212
|
+
2. Identify redundancies
|
|
213
|
+
3. Flag missing coverage
|
|
214
|
+
4. Suggest optimizations
|
|
215
|
+
|
|
216
|
+
## See Also
|
|
217
|
+
|
|
218
|
+
- [Test Layer Decision](guide://test-layer-decision) - Layer decision matrix
|
|
219
|
+
- [Test Mocking Patterns](guide://test-mocking-patterns) - How to stub
|
|
220
|
+
- [Test Suite Health](guide://test-suite-health) - Metrics and audits
|