Dev Unit Testing (Gate 3)
Overview
Ensure every acceptance criterion has at least one unit test proving it works. Follow TDD methodology: RED (failing test) -> GREEN (implementation) -> REFACTOR.
Core principle: Untested acceptance criteria are unverified claims. Each criterion MUST map to at least one executable unit test.
<block_condition>
- Coverage below 85% = FAIL
- Any acceptance criterion without test = FAIL </block_condition>
Coverage threshold: 85% minimum (Ring standard). PROJECT_RULES.md can raise, not lower.
CRITICAL: Role Clarification
This skill ORCHESTRATES. QA Analyst Agent EXECUTES.
| Who | Responsibility |
|---|---|
| This Skill | Gather requirements, dispatch agent, track iterations |
| QA Analyst Agent | Write tests, run coverage, report results |
Step 1: Validate Input
REQUIRED INPUT (from ring:dev-cycle orchestrator):
<verify_before_proceed>
- unit_id exists
- acceptance_criteria is not empty
- implementation_files is not empty
- language is valid (go|typescript|python)
</verify_before_proceed>
- unit_id: [task/subtask being tested]
- acceptance_criteria: [list of ACs to test]
- implementation_files: [files from Gate 0]
- language: [go|typescript|python]
OPTIONAL INPUT:
- coverage_threshold: [default 85.0, cannot be lower]
- gate0_handoff: [full Gate 0 output]
- existing_tests: [existing test files]
if any REQUIRED input is missing: → STOP and report: "Missing required input: [field]" → Return to orchestrator with error
if coverage_threshold < 85: → STOP and report: "Coverage threshold cannot be below Ring minimum (85%)" → Use 85% as threshold
## Step 2: Initialize Testing State
testing_state = { unit_id: [from input], coverage_threshold: max(85, [from input]), coverage_actual: null, verdict: null, iterations: 0, max_iterations: 3, traceability_matrix: [], tests_written: 0, # Goroutine leak detection (Go only) goroutine_check: null, # NOT_APPLICABLE | REQUIRED goroutine_files: 0, # Count of files with goroutines goleak_coverage: null, # "X/Y" packages with goleak leaks_detected: 0, # Count of actual leaks goroutine_verdict: null # PASS | NEEDS_ACTION | FAIL }
## Step 3: Dispatch QA Analyst Agent
<dispatch_required agent="ring:qa-analyst"> Write unit tests for all acceptance criteria with 85%+ coverage. </dispatch_required>
Task: subagent_type: "ring:qa-analyst" description: "Write unit tests for [unit_id]" prompt: | ⛔ WRITE UNIT TESTS for All Acceptance Criteria
## Input Context - Unit ID: [unit_id] - Language: [language] - Coverage Threshold: [coverage_threshold]%
## Acceptance Criteria to Test [list acceptance_criteria with AC-1, AC-2, etc.]
## Implementation Files to Test [list implementation_files]
## Standards Source (Cache-First Pattern)
Standards Source (Cache-First Pattern): This sub-skill reads standards from state.cached_standards populated by dev-cycle Step 1.5. If invoked outside a cycle (standalone), it falls back to direct WebFetch with a warning. See shared-patterns/standards-cache-protocol.md for protocol details.
## Standards Reference
For Go: https://raw.githubusercontent.com/LerianStudio/ring/main/dev-team/docs/standards/golang.md For TS: https://raw.githubusercontent.com/LerianStudio/ring/main/dev-team/docs/standards/typescript.md
Cache-first loading protocol: For each required standards URL: IF state.cached_standards[url] exists: → Read content from state.cached_standards[url].content → Log: "Using cached standard: {url} (fetched {state.cached_standards[url].fetched_at})" ELSE: → WebFetch url (fallback — should not happen if orchestrator ran Step 1.5) → Log warning: "Standard {url} was not pre-cached; fetched inline"
Focus on: Testing Patterns section
## Requirements
### Test Coverage - Minimum: [coverage_threshold]% branch coverage - Every AC MUST have at least one test - Edge cases REQUIRED (null, empty, boundary, error conditions)
### Test Naming - Go: Test{Unit}_{Method}_{Scenario} - TS: describe('{Unit}', () => { it('should {scenario}', ...) })
### Test Structure - One behavior per test - Arrange-Act-Assert pattern - Mock all external dependencies - no database/API calls (unit tests only)
### Edge Cases Required per AC Type
<cannot_skip> - Minimum 3 edge cases per AC type - null, empty, boundary conditions required - Error conditions required </cannot_skip>
| AC Type | Required Edge Cases | Minimum |
|---|---|---|
| Input validation | null, empty, boundary, invalid format | 3+ |
| CRUD operations | not found, duplicate, concurrent | 3+ |
| Business logic | zero, negative, overflow, boundary | 3+ |
| Error handling | timeout, connection failure, retry | 2+ |
### Multi-Tenant Dual-Mode Testing (Go backend only)
Every repository/service test that accesses a resource (PostgreSQL, MongoDB, Redis, S3, RabbitMQ) must verify BOTH modes. The resolvers in lib-commons v5 work transparently — the same code path handles both modes. Tests verify this contract.
Required pattern: Add dual-mode sub-tests for any test that touches a resource:
func TestCreateAccount(t *testing.T) {
modes := []struct {
name string
multiTenant string
}{
{"single-tenant", "false"},
{"multi-tenant", "true"},
}
for _, mode := range modes {
t.Run(mode.name, func(t *testing.T) {
t.Setenv("MULTI_TENANT_ENABLED", mode.multiTenant)
// ... same test logic, same assertions
// Resolvers handle the connection routing transparently
})
}
}What to verify in multi-tenant mode: - Context contains tenant ID → resolver returns tenant-specific connection - Context WITHOUT tenant ID → resolver returns error (not default connection) - Backward compat: no MULTI_TENANT_* env vars → works as single-tenant
What does NOT need dual-mode tests: - Pure business logic (no resource access) - Utility/helper functions - Frontend/TypeScript tests
## Required Output Format
### Test Files Created
| File | Tests | Lines |
|---|---|---|
| [path] | [count] | +N |
### Coverage Report Command: [coverage command] Result:
[paste actual coverage output]| Package/File | Coverage |
|---|---|
| [name] | [X%] |
| TOTAL | [X%] |
### Traceability Matrix
| AC ID | Criterion | Test File | Test Function | Status |
|---|---|---|---|---|
| AC-1 | [criterion text] | [file] | [function] | ✅/❌ |
| AC-2 | [criterion text] | [file] | [function] | ✅/❌ |
### Quality Checks
| Check | Status |
|---|---|
| No skipped tests | ✅/❌ |
| No assertion-less tests | ✅/❌ |
| Edge cases per AC | ✅/❌ |
| Test isolation | ✅/❌ |
### VERDICT Coverage: [X%] vs Threshold [Y%] VERDICT: PASS / FAIL
If FAIL: - Gap Analysis: [what needs more tests] - Files needing coverage: [list with line numbers]
## Step 3.5: Goroutine Leak Detection (Go only)
**⛔ CONDITIONAL: Only execute if `language == "go"`**
After unit tests pass, detect goroutine usage and verify goleak coverage.
See [ring:dev-goroutine-leak-testing](https://github.com/lerianstudio/ring/blob/HEAD/dev-team/skills/dev-unit-testing/../dev-goroutine-leak-testing/SKILL.md) for full detection patterns and dispatch templates. See [architecture.md](https://github.com/lerianstudio/ring/blob/HEAD/dev-team/skills/dev-unit-testing/../../docs/standards/golang/architecture.md#goroutine-leak-detection-mandatory) for goleak standards.
### Detection Logic
if language != "go": → Skip to Step 4
Detect goroutine patterns: "go func(", "go methodCall("
if no goroutine patterns found: → testing_state.goroutine_check = "NOT_APPLICABLE" → Skip to Step 4
Goroutines detected
→ testing_state.goroutine_check = "REQUIRED" → Dispatch ring:qa-analyst with test_mode="goroutine-leak"
### Dispatch
<dispatch_required agent="ring:qa-analyst" test_mode="goroutine-leak"> MUST dispatch with test_mode="goroutine-leak" to detect leaks and verify goleak coverage. </dispatch_required>
### Parse Output and Handle Verdict
| Verdict | Action |
| --- | --- |
| PASS | Proceed to Step 4 |
| NEEDS_ACTION | Dispatch `ring:backend-engineer-golang` to add goleak tests, re-run |
| FAIL | Dispatch `ring:backend-engineer-golang` to fix leaks, re-run |
---
## Step 4: Parse QA Analyst Output
Parse agent output:
- Extract coverage percentage from Coverage Report
- Extract traceability matrix
- Extract verdict
testing_state.coverage_actual = [extracted coverage] testing_state.traceability_matrix = [extracted matrix] testing_state.tests_written = [count from Test Files Created]
if verdict == "PASS" and coverage_actual >= coverage_threshold: → testing_state.verdict = "PASS" → Proceed to Step 6
if verdict == "FAIL" or coverage_actual < coverage_threshold: → testing_state.verdict = "FAIL" → testing_state.iterations += 1 → if iterations >= max_iterations: Go to Step 7 (Escalate) → Go to Step 5 (Dispatch Fix)
## Step 5: Dispatch Fix to Implementation Agent
**Coverage below threshold → Return to Gate 0 for more tests**
Task: subagent_type: "[implementation_agent from Gate 0]" # e.g., "ring:backend-engineer-golang" description: "Add tests to meet coverage threshold for [unit_id]" prompt: | ⛔ COVERAGE BELOW THRESHOLD - Add More Tests
## Current Status - Coverage Actual: [coverage_actual]% - Coverage Threshold: [coverage_threshold]% - Gap: [threshold - actual]% - Iteration: [iterations] of [max_iterations]
## Gap Analysis (from QA) [paste gap analysis from QA output]
## Files Needing Coverage [paste files list from QA output]
## Requirements 1. Add tests to cover the identified gaps 2. Focus on edge cases and error paths 3. Run coverage after each addition 4. Stop when coverage >= [threshold]%
## Required Output - Tests added: [list] - New coverage: [X%] - Coverage command output
After fix → Go back to Step 3 (Re-dispatch QA Analyst)
## Step 6: Prepare Success Output
Generate skill output:
Testing Summary
Status: PASS Unit ID: [unit_id] Iterations: [testing_state.iterations]
Coverage Report
Threshold: [coverage_threshold]% Actual: [coverage_actual]% Status: ✅ PASS
| Package/File | Coverage |
|---|
[from QA output] | TOTAL | [coverage_actual]% |
Traceability Matrix
| AC ID | Criterion | Test | Status |
|---|
[from testing_state.traceability_matrix]
Criteria Covered: [X]/[Y] (100%)
Quality Checks
| Check | Status |
|---|---|
| Coverage ≥ threshold | ✅ |
| All ACs tested | ✅ |
| No skipped tests | ✅ |
| Edge cases present | ✅ |
| Goroutine leak check (Go only) | [✅/N/A] |
Goroutine Leak Report (Go only)
[If language == "go" and goroutines detected]
| Metric | Value |
|---|---|
| Goroutines detected | [count] |
| Packages with goleak | [X]/[Y] |
| Leaks found | 0 |
| Status | ✅ PASS |
[If language != "go" or no goroutines: "N/A - No goroutines detected"]
Handoff to Next Gate
- Testing status: COMPLETE
- Coverage: [coverage_actual]% (threshold: [coverage_threshold]%)
- All criteria tested: ✅
- Ready for Gate 4 (Review): YES
## Step 7: Escalate - Max Iterations Reached
Generate skill output:
Testing Summary
Status: FAIL Unit ID: [unit_id] Iterations: [max_iterations] (MAX REACHED)
Coverage Report
Threshold: [coverage_threshold]% Actual: [coverage_actual]% Gap: [threshold - actual]% Status: ❌ FAIL
Gap Analysis
[from last QA output]
Files Still Needing Coverage
[from last QA output]
Handoff to Next Gate
- Testing status: FAILED
- Ready for Gate 4: no
- Action Required: User must manually add tests or adjust scope
⛔ ESCALATION: Max iterations (3) reached. Coverage still below threshold. User intervention required.
---
## Severity Calibration
| Severity | Criteria | Examples |
| --- | --- | --- |
| **CRITICAL** | Test infrastructure broken, no coverage possible | Test framework failure, build broken |
| **HIGH** | Coverage below threshold, missing AC tests | 84% coverage (below 85%), untested acceptance criteria |
| **MEDIUM** | Test quality issues, edge case gaps | Missing edge case tests, poor assertion messages |
| **LOW** | Test naming, documentation gaps | Non-standard test names, missing test descriptions |
Report all severities. CRITICAL/HIGH = immediate fix. MEDIUM = fix in iteration. LOW = document for follow-up.
---
## Pressure Resistance
See [shared-patterns/shared-pressure-resistance.md](https://github.com/lerianstudio/ring/blob/HEAD/dev-team/skills/dev-unit-testing/../shared-patterns/shared-pressure-resistance.md) for universal pressure scenarios.
| User Says | Your Response |
| --- | --- |
| "84% is close enough" | "85% is minimum threshold. 84% = FAIL. Adding more tests." |
| "Manual testing covers it" | "Gate 3 requires executable unit tests. Dispatching QA analyst." |
| "Skip testing, deadline" | "Testing is MANDATORY. Untested code = unverified claims." |
---
## Anti-Rationalization Table
See [shared-patterns/shared-anti-rationalization.md](https://github.com/lerianstudio/ring/blob/HEAD/dev-team/skills/dev-unit-testing/../shared-patterns/shared-anti-rationalization.md) for universal anti-rationalizations.
### Gate 3-Specific Anti-Rationalizations
| Rationalization | Why It's WRONG | Required Action |
| --- | --- | --- |
| "Tool shows 83% but real is 90%" | Tool output IS real. Your belief is not. | **Fix issue, re-measure** |
| "Excluding dead code gets us to 85%" | Delete dead code, don't exclude it. | **Delete dead code** |
| "84.5% rounds to 85%" | Rounding is not allowed. 84.5% < 85%. | **Write more tests** |
| "Close enough with all AC tested" | "Close enough" is not passing. | **Meet exact threshold** |
| "Integration tests cover this" | Gate 3 = unit tests only. Different scope. | **Write unit tests** |
## Unit Test vs Integration Test
| Type | Characteristics | Gate 3? |
| --- | --- | --- |
| **Unit** ✅ | Mocks all external deps, tests single function | YES |
| **Integration** ❌ | Hits real database/API/filesystem | no |
---
## Execution Report Format
Testing Summary
Status: [PASS|FAIL] Unit ID: [unit_id] Duration: [Xm Ys] Iterations: [N]
Coverage Report
Threshold: [X%] Actual: [Y%] Status: [✅ PASS | ❌ FAIL]
Traceability Matrix
| AC ID | Criterion | Test | Status |
|---|---|---|---|
| AC-1 | [text] | [test] | ✅/❌ |
Criteria Covered: [X/Y]
Handoff to Next Gate
- Testing status: [COMPLETE|FAILED]
- Coverage: [X%]
- Ready for Gate 4: [YES|no]