Managed-By: skai Managed-Id: guide.unit-test-planning Managed-Source: Guides/Test/unit-test-planning-guide.md Managed-Adapter: repo-source Managed-Updated-At: 2026-03-07
Defines the process for creating complete test plans with ALL test stubs and documentation across ALL sections before any implementation begins.
Use these standard gate lines:
- Planned gate:
β³ GATE: Next: <what happens after your response>. Say "next" or what to change. - Blocked gate:
β³ GATE: Blocked: <reason>. Resolve and say "next" to continue.
Planned gates are the expected review points of this workflow. At each planned gate:
- Summarize what was planned and what should happen next.
- End with the planned gate line.
- STOP and wait for the human.
In the planned gate line, <what happens after your response> should describe what the agent will do after the human gives advance intent.
If an unexpected blocker prevents continued work, use the blocked gate line and STOP until the human resolves it.
When the workflow finishes, return control to the parent testing workflow.
Planned gates for this workflow:
- After planning is complete (all files/sections/tests are stubbed and marked π‘).
Advance intent moves past the current gate. Common signals: "next", "continue", "go ahead", "do it".
Rules:
- Recognized as approval to move past a gate only after you output a
β³ GATE:line. - "we should...", "let's..." = discussion/context-setting, NOT authorization.
- Outside a gate, interpret "begin"/"next"/"continue" as "resume planning where you left off." Do not use them to clear planning markers early.
Progress tracking:
- Default rule: π‘ = TODO or pending approval. Do not clear π‘ without human approval.
- In this workflow, section-level and test-level π‘ markers in the planned test files remain in place when planning completes.
- Advance intent at the planning-complete gate returns control to the parent testing workflow; it does not clear the planned test sections/tests.
- Planning ambiguities should stop with the blocked gate line and leave all planning markers in place.
Behavior: Context determines the action:
- If waiting at the planning-complete gate β return control to the parent testing workflow
- If stopped due to planning ambiguities or unexpected challenges β resume planning where you left off
Goal: Create test plan with stubs and detailed doc comments
Output: One or more test files with sections and tests marked π‘ to indicate TODO
Four scenarios:
-
New Test Suite - Creating test file from scratch
- Create test file with suite declaration
- Define ALL MARK sections
- Add π‘ emoji to all section MARK comments (marking sections as TODO)
- Add ALL test stubs across ALL sections
- Add π‘ emoji to each test function (marking individual tests as TODO)
- Add detailed doc comments for every test
-
Adding New Sections - Adding new sections to existing test file
- Open existing test file
- Define new MARK sections being added
- Add π‘ emoji to new section MARK comments (marking sections as TODO)
- Add all test stubs for the new sections only
- Add π‘ emoji to each new test function (marking individual tests as TODO)
- Add detailed doc comments for every new test
- Existing sections and tests remain unchanged
-
Adding Tests to Existing Section - Adding tests within an existing section
- Open existing test file
- Locate the existing MARK section
- Add π‘ emoji to section MARK comment if not already present (marking section as TODO)
- Add new test stubs within the section
- Add π‘ emoji to each new test function (marking individual tests as TODO)
- Add detailed doc comments for every new test
- Existing tests in section remain unchanged
-
Marking Failing Tests - Identifying existing tests that need fixing
- Open existing test file
- Locate the failing test functions
- Add π‘ emoji to each failing test function (marking them as needing fixes)
- Add π‘ emoji to section MARK comment if not already present (marking section as TODO)
- Present marked tests for approval
Multi-File Planning: When planning tests for multiple types:
- Create ALL test files in a single planning phase
- Each file follows the same structure (sections with π‘, test stubs with π‘)
- Present the complete list of all planned files at the planning-complete gate
- Implementation proceeds file-by-file after planning approval
CRITICAL:
- Do all planning upfront (all files, all sections, all tests)
- Mark sections with π‘ to indicate they contain TODO work
- Mark individual test functions with π‘ to indicate they need implementation or fixes
- Do NOT create new work documents during planning
- Only make changes that qualify as "test planning" - nothing else
What qualifies as test planning:
- β Create new test file with suite declaration (new suite only)
- β Add new MARK sections with π‘
- β
Add new test stubs with π‘ and
Issue.record() - β Add π‘ to existing section MARK if adding tests to it
- β Add doc comments to new test stubs
What does NOT qualify as test planning:
- β Renaming existing tests
- β Refactoring existing test code
- β Changing
structtoclassor other structural changes - β Modifying cleanup patterns
- β Reorganizing existing MARK sections
- β Any change to existing code
Key principle: Test stubs with detailed doc comments ARE the plan
CRITICAL: The doc comments in the code ARE the test plan. They must be sufficient for review and approval.
Planning creates:
For New Test Suite:
- Test file with ALL test stubs across ALL sections
- ALL MARK sections defined with π‘ emoji (marking sections as TODO)
- ALL test functions with π‘ emoji (marking individual tests as TODO)
- Detailed doc comments for every test
- Complete picture of test coverage
For Adding New Sections:
- New MARK sections in existing test file with π‘ emoji (marking sections as TODO)
- Test stubs for new sections only with π‘ emoji (marking individual tests as TODO)
- Detailed doc comments for every new test
- Existing sections and tests remain unchanged
For Adding Tests to Existing Section:
- New test stubs in existing section with π‘ emoji (marking individual tests as TODO)
- Section MARK comment with π‘ emoji if not already present (marking section as TODO)
- Detailed doc comments for every new test
- Existing tests in section remain unchanged
For Marking Failing Tests:
- Failing test functions marked with π‘ emoji (indicating need for fixes)
- Section MARK comment with π‘ emoji if not already present (marking section as TODO)
All tests require:
- Descriptive camelCase name
@Test("...")display name -- concise behavior sentence for test navigator and CI output (not a duplicate of the function name)- Concise, but sufficient doc comment that explains what the test verifies and how it verifies it (enough for human review/approval).
- Prefer 1-3 short lines; add more only when needed for clarity.
- Body with
Issue.record("Test not yet implemented")to mark as failing TODO
Doc Comment Guidelines:
- Keep it concise, but sufficient to describe what the test verifies and how it verifies it
- Be specific about the behavior being validated; include key setup only if it is not obvious
- Make it clear enough for human review and approval
Creating Complete Test Plan:
For New Test Suite:
- Create test file with suite declaration
- Define ALL MARK sections for logical grouping
- Add π‘ emoji to all MARK sections (marking sections as TODO)
- Add ALL test stub functions across ALL sections
- Add π‘ emoji to each test function (marking individual tests as TODO)
- Add detailed doc comments for every test
- Add standard sections: Constants, Variables, Helpers
For Adding New Sections:
- Open existing test file
- Define new MARK sections for new functionality
- Add π‘ emoji to new MARK sections (marking sections as TODO)
- Add test stub functions for new sections only
- Add π‘ emoji to each new test function (marking individual tests as TODO)
- Add detailed doc comments for every new test
- Existing sections and helpers remain unchanged
For Adding Tests to Existing Section:
- Open existing test file
- Locate the existing MARK section
- Add π‘ emoji to section MARK comment if not already present (marking section as TODO)
- Add new test stub functions within the section
- Add π‘ emoji to each new test function (marking individual tests as TODO)
- Add detailed doc comments for every new test
- Existing tests in section remain unchanged
For Marking Failing Tests:
- Open existing test file
- Locate the failing test functions
- Add π‘ emoji to each failing test function (marking them as needing fixes)
- Add π‘ emoji to section MARK comment if not already present (marking section as TODO)
Example (Swift Testing framework):
import Testing
import Factory
@testable import YourModule
@Suite(.serialized) @MainActor
struct ComponentTests {
// MARK: - Success Tests π‘
/// Verifies that a successful operation returns the expected value.
/// Stubs the dependency to return a success response.
/// Ensures the component properly processes the response.
@Test("Returns expected value on success") func testOperationReturnsSuccessResponse() async throws { // π‘
Issue.record("Test not yet implemented")
}
/// Verifies that required parameters are passed correctly.
/// Creates an operation with specific parameters and inspects the captured values.
/// Validates that all parameters are included correctly.
@Test("Passes required parameters to dependency") func testOperationPassesParameters() async throws { // π‘
Issue.record("Test not yet implemented")
}
// MARK: - Error Handling Tests π‘
/// Verifies that error conditions throw appropriate errors.
/// Stubs dependency to return error condition. Ensures component throws appropriate error.
@Test("Throws appropriate error on failure") func testOperationThrowsOnError() async throws { // π‘
Issue.record("Test not yet implemented")
}
// MARK: - Constants
// MARK: - Variables
// MARK: - Helpers
}Assertions - Use #expect() (not XCTest macros):
#expect(value == expected) // Equality
#expect(value != unexpected) // Inequality
#expect(value > 0) // Comparison
#expect(optionalValue == nil) // Nil check
#expect(throws: MyError.self) { // Error throwing
try somethingThatThrows()
}CRITICAL: Unit tests should test behavior through PUBLIC interfaces
Rules:
- Only test public and internal methods - Never create tests for
privatemethods - Private methods are tested indirectly - They're exercised through the public methods that call them
- Focus on behavior, not implementation - Test what the code does, not how it does it
- Don't expose internals for testing by default - In most cases, indirect testing through public APIs is sufficient
- Exception: Request visibility change when needed - If testing a private method through public APIs is problematic, you can request it be made
internalfor testing
Why:
- Tests should validate contracts and behavior, not implementation details
- Private methods can be refactored freely if tests focus on public APIs
- Indirect testing ensures private methods work in their actual usage context
- Exposing internals for testing creates artificial coupling
Planning Check: Before creating test stubs for a method, verify:
- β
Is it
publicorinternal? β Create tests - β Is it
private? β Check if it's tested through public APIs- If adequately tested indirectly β Do NOT create tests
- If testing through public APIs is problematic β Request it be made
internalfor testing
When to request visibility change:
- Complex logic that's difficult to trigger through public APIs
- Need for comprehensive edge case testing that's impractical via public methods
- Method has critical business logic that warrants direct testing
- Document the reason in your work document when requesting the change
Example:
class ComponentUnderTest {
// β
Test this directly - it's the public API
public func processData() async throws -> Result {
let validated = validateInput() // private helper
return try await transform(validated) // private helper
}
// β Don't test these directly - they're private
private func validateInput() -> ValidationResult { ... }
private func transform(_ data: ValidationResult) async throws -> Result { ... }
}
// Tests should only call processData()
// validateInput() and transform() are tested indirectly through processData()During Planning:
- When reviewing the source code to plan tests, check method visibility
- Only create test stubs for public/internal methods
- Trust that private methods will be adequately tested through public APIs
CRITICAL: Every test must validate actual logic, not language features or redundancy
Frivolous tests to avoid:
-
Testing protocol conformance - The compiler guarantees this
// β BAD: Testing that a protocol method returns the required type @Test func testErrorConvertibleReturnsAIError() async throws { let error = CustomError.failure #expect(error.asAIError is AIError) // Compiler already guarantees this }
-
Testing trivial property accessors - No logic to test
// β BAD: Testing a property that's just optional chaining var currentOut: Output? { currentVersionedOut?.output } @Test func testCurrentOutMatchesVersionedOut() { // This just tests Swift's optional chaining - no actual logic }
-
Testing standard library/framework behavior - Already tested by Apple/Swift
// β BAD: Testing that Combine's .map() works var versions: Publisher { versionedOut.map { $0?.versions } } @Test func testVersionsPublisherExtractsVersions() { // This just tests Combine's .map() - no actual logic }
-
Testing implementation details - Tests private internals, not behavior
// β BAD: Testing internal state cleanup @Test func testContinuationCleanedUpAfterEmission() { // versionedOutContinuation is a private implementation detail }
-
Testing the inverse of another test - Redundant coverage
// β BAD: Testing "not stale" when you already test "is stale" @Test func testIsStaleDetectsChangedInputs() { ... } // β Good @Test func testIsStaleIsFalseWhenUnchanged() { ... } // β Redundant
-
Testing already-covered behavior - Duplicate coverage
// β BAD: Both test version incrementing @Test func testPerformIncrementsVersion() { // Verifies version 0, 1, 2 } @Test func testVersionNumberStartsCorrectly() { // Verifies version starts at 0 - already covered above }
What TO test:
β Business logic - Actual decisions and transformations β State management - Complex state transitions and side effects β Error handling - Error capture, propagation, and recovery β Integration behavior - How components work together β Edge cases - Boundary conditions and unusual inputs β Observable behavior - What callers can see and rely on
Planning checklist to avoid frivolous tests:
Before creating a test stub, ask:
- β Does this test actual logic? If it's just calling a getter, probably no.
- β Could this test fail if I changed the implementation? If only breaking changes would fail it, it's testing the type system.
- β Is this already covered by another test? Check for redundancy.
- β Am I testing my code or the framework? Don't test Swift/Combine/Foundation.
- β Is this testing observable behavior or implementation details? Stick to public contracts.
Examples of good vs bad tests:
// β
GOOD: Tests actual lock behavior logic
@Test func testLockedReturnsExistingOutput() {
// Tests that .locked prevents re-execution - real business logic
}
// β BAD: Tests trivial accessor
@Test func testCurrentOutReturnsOutputFromVersionedOut() {
// currentOut is just: currentVersionedOut?.output
// No logic - just optional chaining
}
// β
GOOD: Tests error capture and storage logic
@Test func testPerformCapturesError() {
// Tests actual error handling behavior
}
// β BAD: Tests protocol conformance
@Test func testCustomErrorConvertsToAIError() {
// Protocol conformance is a compile-time guarantee
}
// β
GOOD: Tests version incrementing logic
@Test func testPerformIncrementsVersion() {
// Verifies versions: 0, 1, 2 - tests actual logic
}
// β BAD: Redundant with above
@Test func testVersionNumberStartsCorrectly() {
// Just checks version 0 - already covered above
}Use Swift Testing framework (not XCTest) for all new test suites
Framework imports:
import Testing // β
Use Swift Testing
@testable import ModuleName
@Suite(.serialized) @MainActor
struct MyComponentTests {
@Test func testSomething() async throws {
defer { Container.shared.reset() }
// Use #expect() for assertions
}
}Key points:
- Use
structfor test suites - Use
defer { Container.shared.reset() }at the start of each test that registers dependencies .serializedprevents DI override conflicts between tests@MainActorensures tests run on main actor when needed
See docs/skai/integration.md for:
- Test target organization
- File naming conventions
- Test file structure
Use MARK comments to organize (following code-organization pattern):
// MARK: - Send Tests (or just "Tests", or multiple sections for finer grouping)
// MARK: - Constants
// MARK: - Variables
// MARK: - HelpersNote: Test sections come FIRST, before Constants/Variables/Helpers
- Function names: camelCase (e.g.,
testOperationReturnsSuccessResponse) - Display names:
@Test("...")with a concise behavior sentence (e.g.,@Test("Returns expected value on success")) - Documentation: Plain English doc comment explaining the test
- Example:
/// Verifies that a successful operation returns the expected value @Test("Returns expected value on success") func testOperationReturnsSuccessResponse() async throws { // TODO }
- Flexible - whatever makes sense for the scenario
- Can be one assertion or multiple related assertions
- Can be small focused tests or longer scenario/integration tests
- Think about happy paths AND error paths
- Consider edge cases and boundary conditions
- Don't duplicate coverage - each test should verify something unique
- Group related tests into logical MARK sections
- Use
testprefix - Use descriptive verb + object pattern:
testOperationReturnsSuccessResponse - Be specific:
testErrorConditionTriggersRetrynottestHandlesError
- Doc comments should explain what the test verifies (and briefly how it verifies it if not obvious)
- Keep it concise, but sufficient for human review/approval
- Focus on behavior, not implementation details
Creating Complete Test Plan:
- Identify all functional areas to test
- Define MARK sections for logical grouping
- Create stub functions with descriptive names for ALL tests
- Add detailed doc comments for every test
- Leave all bodies empty or with
// TODO - Follow MARK organization pattern
- Present complete plan to human
- Wait for human to choose which section to start
Planning Output:
- New Suite: Create ALL test stubs across ALL sections with π‘ emoji on sections and tests,
Issue.record()in body - Adding New Sections: Create test stubs for new sections only with π‘ emoji on sections and tests,
Issue.record()in body - Adding Tests to Section: Create test stubs within existing section with π‘ emoji on tests (and section if needed),
Issue.record()in body - Marking Failing Tests: Mark existing failing tests with π‘ emoji (and section if needed)
- Detailed doc comments for every test
- Organized by MARK sections
Issue.record("Test not yet implemented")in stub bodies to mark as failing TODO
Test Suite Structure (new files only):
- Use
@Suite(.serialized) @MainActor struct - Use
defer { Container.shared.reset() }in tests that register dependencies - Test sections first, then Constants, Variables, Helpers
Only make changes that qualify as "test planning":
- Adding new sections and test stubs = planning β
- Renaming, refactoring, restructuring = NOT planning β
- Structural changes to existing tests = NOT planning β