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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/protocols/agent-sources/ace-test.yml +19 -0
  3. data/.ace-defaults/nav/protocols/guide-sources/ace-test.yml +19 -0
  4. data/.ace-defaults/nav/protocols/tmpl-sources/ace-test.yml +11 -0
  5. data/.ace-defaults/nav/protocols/wfi-sources/ace-test.yml +19 -0
  6. data/CHANGELOG.md +169 -0
  7. data/LICENSE +21 -0
  8. data/README.md +40 -0
  9. data/Rakefile +12 -0
  10. data/handbook/agents/mock.ag.md +164 -0
  11. data/handbook/agents/profile-tests.ag.md +132 -0
  12. data/handbook/agents/test.ag.md +99 -0
  13. data/handbook/guides/SUMMARY.md +95 -0
  14. data/handbook/guides/embedded-testing-guide.g.md +261 -0
  15. data/handbook/guides/mocking-patterns.g.md +464 -0
  16. data/handbook/guides/quick-reference.g.md +46 -0
  17. data/handbook/guides/test-driven-development-cycle/meta-documentation.md +26 -0
  18. data/handbook/guides/test-driven-development-cycle/ruby-application.md +18 -0
  19. data/handbook/guides/test-driven-development-cycle/ruby-gem.md +19 -0
  20. data/handbook/guides/test-driven-development-cycle/rust-cli.md +18 -0
  21. data/handbook/guides/test-driven-development-cycle/rust-wasm-zed.md +19 -0
  22. data/handbook/guides/test-driven-development-cycle/typescript-nuxt.md +18 -0
  23. data/handbook/guides/test-driven-development-cycle/typescript-vue.md +19 -0
  24. data/handbook/guides/test-layer-decision.g.md +261 -0
  25. data/handbook/guides/test-mocking-patterns.g.md +414 -0
  26. data/handbook/guides/test-organization.g.md +140 -0
  27. data/handbook/guides/test-performance.g.md +353 -0
  28. data/handbook/guides/test-responsibility-map.g.md +220 -0
  29. data/handbook/guides/test-review-checklist.g.md +231 -0
  30. data/handbook/guides/test-suite-health.g.md +337 -0
  31. data/handbook/guides/testable-code-patterns.g.md +315 -0
  32. data/handbook/guides/testing/ruby-rspec-config-examples.md +120 -0
  33. data/handbook/guides/testing/ruby-rspec.md +87 -0
  34. data/handbook/guides/testing/rust.md +52 -0
  35. data/handbook/guides/testing/test-maintenance.md +364 -0
  36. data/handbook/guides/testing/typescript-bun.md +47 -0
  37. data/handbook/guides/testing/vue-firebase-auth.md +546 -0
  38. data/handbook/guides/testing/vue-vitest.md +236 -0
  39. data/handbook/guides/testing-philosophy.g.md +82 -0
  40. data/handbook/guides/testing-strategy.g.md +151 -0
  41. data/handbook/guides/testing-tdd-cycle.g.md +146 -0
  42. data/handbook/guides/testing.g.md +170 -0
  43. data/handbook/skills/as-test-create-cases/SKILL.md +24 -0
  44. data/handbook/skills/as-test-fix/SKILL.md +26 -0
  45. data/handbook/skills/as-test-improve-coverage/SKILL.md +22 -0
  46. data/handbook/skills/as-test-optimize/SKILL.md +34 -0
  47. data/handbook/skills/as-test-performance-audit/SKILL.md +34 -0
  48. data/handbook/skills/as-test-plan/SKILL.md +34 -0
  49. data/handbook/skills/as-test-review/SKILL.md +34 -0
  50. data/handbook/skills/as-test-verify-suite/SKILL.md +45 -0
  51. data/handbook/templates/e2e-sandbox-checklist.template.md +289 -0
  52. data/handbook/templates/test-case.template.md +56 -0
  53. data/handbook/templates/test-performance-audit.template.md +132 -0
  54. data/handbook/templates/test-responsibility-map.template.md +92 -0
  55. data/handbook/templates/test-review-checklist.template.md +163 -0
  56. data/handbook/workflow-instructions/test/analyze-failures.wf.md +120 -0
  57. data/handbook/workflow-instructions/test/create-cases.wf.md +675 -0
  58. data/handbook/workflow-instructions/test/fix.wf.md +120 -0
  59. data/handbook/workflow-instructions/test/improve-coverage.wf.md +370 -0
  60. data/handbook/workflow-instructions/test/optimize.wf.md +368 -0
  61. data/handbook/workflow-instructions/test/performance-audit.wf.md +17 -0
  62. data/handbook/workflow-instructions/test/plan.wf.md +323 -0
  63. data/handbook/workflow-instructions/test/review.wf.md +16 -0
  64. data/handbook/workflow-instructions/test/verify-suite.wf.md +343 -0
  65. data/lib/ace/test/version.rb +7 -0
  66. data/lib/ace/test.rb +10 -0
  67. 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