Guide for writing unit tests in isimctl using Swift Testing and Mockolo. Use when asked to create tests, add test coverage, write test cases, generate stubs, fix failing tests, or work with mocks. Covers test planning, naming conventions, Given-When-Then pattern, mock argument verification, stub creation, and test-specific linting.
Comprehensive guide for writing unit tests in isimctl using the Swift Testing framework and Mockolo-generated mocks.
make gen-mocks after adding/modifying @mockable protocolsTests/<Module>Tests/)Sources/IsimctlUI/ → Tests/IsimctlUITests/)Sources/<Target>/<Path>/<File>.swift → Tests/<Target>Tests/<Path>/<File>Tests.swift
DeviceSelectionPrompt.swift in Sources/IsimctlUI/Shared/ → DeviceSelectionPromptTests.swift in Tests/IsimctlUITests/Shared/Tests suffix.Decision rule: Use unit tests for business logic and UI components with mocked dependencies.
Tests/SimctlKitIntegrationTests/)xcrun simctl commands without mocks.Decision rule: Use integration tests sparingly for critical simctl interactions requiring real system validation.
Tests/<Module>Mocks/)@mockable protocols via Mockolo.Tests/<Module>Mocks/<Module>Mocks.generated.swiftmake gen-mocks after adding/modifying @mockable protocols.Decision rule: Never edit mock files manually. Always regenerate with make gen-mocks.
Before generating test code, identify all test scenarios covering normal cases, edge cases, and error handling paths. When asked to create a test pattern list, enumerate scenarios systematically based on code branches and conditions.
Group related test cases using MARK comments:
// MARK: - Normal Cases
// MARK: - Edge Cases
// MARK: - Error Handling
errorAlert.show(error)), testing each error variant separately adds no value.All test files and test struct names must use the Tests suffix (plural form):
<TargetName>Tests.swift<TargetName>Tests// SimctlTests.swift
struct SimctlTests {
// Test implementations
}
Rules:
<TargetName> with the name of the component, type, or functionality being tested.swift)Tests (plural) as the suffixTest case names must start with the function name under test, followed by a description in camel case.
Do not use the displayName parameter in the @Test attribute. The function name itself should be descriptive enough.
Good:
@Test
func functionName_shouldDoSomethingWhenConditionIsMet() {
// Test implementation
}
Bad (avoid this):
@Test("functionName_shouldDoSomethingWhenConditionIsMet")
func functionName_shouldDoSomethingWhenConditionIsMet() {
// Test implementation
}
Use the Given-When-Then structure for complex test scenarios:
// Given: Setup test data and mock behaviors
let device = Device(name: "iPhone 16 Pro")
mock.handler = { _ in device }
// When: Execute the code under test
let result = try await service.fetchDevice()
// Then: Verify expectations
#expect(result == device)
#expect(mock.fetchDeviceCallCount == 1)
// Given:, // When:, // Then:) to document intentWhen using Mockolo-generated mocks, verify that methods are called with the correct arguments:
// Then: Verify method was called with correct arguments
#expect(simctl.listDevicesArgValues == ["booted"])
This ensures both the count and content of arguments match expectations.
Test data helpers are centralized in dedicated stub files within Mocks targets.
Tests/<Target>Mocks/Stub/<TypeName or FileName>+Stub.swift (e.g., RuntimeDeviceGroupOption+Stub.swift, SimulatorList+Stub.swift)extension TargetType {
/// Creates a test stub with customizable parameters.
static func stub(
param1: String = "default",
param2: Int = 0,
) -> Self {
.init(param1: param1, param2: param2)
}
}
When writing test code, it is acceptable to disable specific SwiftLint rules:
type_body_length: Test structs often exceed body length limits.file_length: Test files may grow large when covering all scenarios.Add a swiftlint:disable comment at the top of the test file:
// swiftlint:disable type_body_length file_length
import Testing
@testable import YourModule
Only disable these rules when the violation is unavoidable and justified by comprehensive test coverage.
| Task | Command |
|---|---|
| Run all unit tests | swift test 2>&1 |
| Run specific test target | swift test --filter <TestTargetName> 2>&1 |
| Regenerate mocks | make gen-mocks |
| Issue | Solution |
|---|---|
| Mock type not found | Run make gen-mocks after adding @mockable to the protocol |
| Test not discovered | Ensure test struct/function naming follows conventions above |
| Handler not called | Check that the mock's handler property is set before executing the code under test |
| argValues is empty | Verify the mock was injected correctly via the internal initializer |