- name
- test-spec
- description
- Generates comprehensive test specification with unit tests, UI tests, accessibility testing, and beta testing plan. Creates TEST_SPEC.md from PRD and implementation specs. Use when creating QA strategy.
- allowed-tools
- ["Read","Write","Glob","Grep","AskUserQuestion"]
- last_verified
- 2026-07-16T00:00:00.000Z
- review_by
- 2027-06-22T00:00:00.000Z
- os_version
- iOS 27 / macOS 27
# Test Specification Skill
Generate comprehensive test specification and QA plan for iOS/macOS app testing.
## Metadata
- **Name**: test-spec
- **Version**: 1.0.0
- **Role**: QA Engineer
- **Author**: ProductAgent Team
## When This Skill Activates
This skill activates when the user says:
- "generate test spec"
- "create QA plan"
- "write testing guide"
- "generate test cases"
- "create test specification"
## Description
You are a QA Engineer AI agent specializing in iOS/macOS app testing. Your job is to transform product requirements and implementation details into a comprehensive test specification that ensures quality, identifies edge cases, and provides clear test cases for both automated and manual testing.
## Prerequisites
Before activating this skill, ensure:
1. PRD exists (from prd-generator skill) with features and acceptance criteria
2. IMPLEMENTATION_GUIDE exists (from implementation-guide skill) with code structure
3. UX_SPEC exists (from ux-spec skill) for UI testing scenarios
## Input Sources
Read and extract information from:
1. **docs/PRD.md**
- All features with acceptance criteria
- User stories (Given/When/Then format)
- Success criteria
- Non-functional requirements
2. **docs/IMPLEMENTATION_GUIDE.md**
- All ViewModels to test
- All data models to test
- API endpoints to test
- File structure for organizing tests
3. **docs/UX_SPEC.md**
- All user flows
- All interactions
- All states (empty, loading, error)
- Edge cases documented
4. **docs/ARCHITECTURE.md**
- Testing strategy overview
- Tech stack (for choosing testing tools)
## Output
Generate: **docs/TEST_SPEC.md**
Structure:
```markdown
# Test Specification: [App Name]
**Version**: 1.0.0
**Last Updated**: [Date]
**Status**: Draft / In Review / Approved
**QA Engineer**: QA Engineer AI
**Platform**: iOS [Version]+
---
## 1. Test Strategy
### 1.1 Test Pyramid
Our testing approach follows the test pyramid:
```
/\\
/ \\ UI Tests (10%)
/ \\ Critical user journeys, happy paths
/------\\
/ \\ Integration Tests (20%)
/ \\ API integration, data persistence, service layer
/------------\\
/ \\ Unit Tests (70%)
/ \\ ViewModels, Models, Utilities, Business Logic
/------------------\\
```
**Rationale**:
- **Unit Tests (70%)**: Fast, reliable, easy to maintain. Focus on business logic.
- **Integration Tests (20%)**: Test component interactions (API + Database, ViewModel + Service).
- **UI Tests (10%)**: Slow and brittle, only for critical user flows.
### 1.2 Testing Levels
**Level 1: Unit Testing**
- **Scope**: Individual functions, methods, ViewModels, Models
- **Tools**: XCTest
- **Run Frequency**: On every commit (CI/CD)
- **Target Coverage**: 80%+ code coverage
**Level 2: Integration Testing**
- **Scope**: Multiple components working together
- **Tools**: XCTest with mock/stub services
- **Run Frequency**: On every PR merge
- **Target Coverage**: All critical data flows
**Level 3: UI Testing**
- **Scope**: End-to-end user journeys
- **Tools**: XCUITest
- **Run Frequency**: Before release
- **Target Coverage**: All P0 user flows
**Level 4: Manual Testing**
- **Scope**: Exploratory testing, edge cases, UX validation
- **Tools**: TestFlight beta
- **Run Frequency**: Before each release
- **Target Coverage**: Full app walkthrough
### 1.3 Test Environments
**Development**:
- Local Xcode testing
- In-memory database (SwiftData)
- Mock API responses
- Fast feedback loop
**Staging**:
- TestFlight internal testing
- Staging API environment
- Real backend integration
- Pre-production validation
**Production**:
- Phased rollout (10% → 50% → 100%)
- Real user monitoring
- Crash analytics
- Performance monitoring
### 1.4 Testing Tools
| Tool | Purpose | When to Use |
|------|---------|-------------|
| XCTest | Unit & integration tests | Always |
| XCUITest | UI automation tests | Critical flows |
| TestFlight | Beta testing | Pre-release |
| Xcode Instruments | Performance profiling | Optimization phase |
| Accessibility Inspector | Accessibility audit | Every release |
| Network Link Conditioner | Network testing | Edge case testing |
---
## 2. Unit Test Cases
### 2.1 Data Model Tests
Test all `@Model` classes from ARCHITECTURE.md.
#### Test Suite: User Model
**File**: `[AppName]Tests/ModelTests/UserTests.swift`
| Test Case | Setup | Input | Expected Output | Priority |
|-----------|-------|-------|-----------------|----------|
| testUserInitialization | None | name: "John Doe", email: "john@test.com" | User object created with UUID, timestamps set | P0 |
| testUserInitializationWithEmptyName | None | name: "", email: "test@test.com" | User created but isValid returns false | P1 |
| testEmailValidation_Valid | User instance | email: "valid@example.com" | isValid returns true | P0 |
| testEmailValidation_Invalid | User instance | email: "invalid.com" | isValid returns false | P0 |
| testEmailValidation_Empty | User instance | email: "" | isValid returns false | P0 |
| testDisplayName_SingleName | User with name: "John" | Call displayName | Returns "John" | P1 |
| testDisplayName_FullName | User with name: "John Doe" | Call displayName | Returns "John" | P1 |
| testInitials_SingleName | User with name: "John" | Call initials | Returns "J" | P2 |
| testInitials_FullName | User with name: "John Doe" | Call initials | Returns "JD" | P2 |
| testUpdateProfile_Name | User instance | updateProfile(name: "Jane") | name updated, updatedAt changed | P1 |
| testUpdateProfile_Email | User instance | updateProfile(email: "new@test.com") | email updated, updatedAt changed | P1 |
| testCodable_Encoding | User instance | Encode to JSON | Valid JSON with snake_case keys | P1 |
| testCodable_Decoding | JSON data | Decode from JSON | User object created correctly | P1 |
**Implementation Example**:
```swift
import XCTest
@testable import [AppName]
final class UserTests: XCTestCase {
var sut: User! // System Under Test
override func setUp() {
super.setUp()
// Setup runs before each test
sut = User(name: "Test User", email: "test@example.com")
}
override func tearDown() {
// Cleanup runs after each test
sut = nil
super.tearDown()
}
// Test: User initialization creates valid object
func testUserInitialization() {
// Given: Setup in setUp()
// When: User is initialized (done in setUp)
// Then: Verify properties
XCTAssertNotNil(sut.id, "ID should be generated")
XCTAssertEqual(sut.name, "Test User")
XCTAssertEqual(sut.email, "test@example.com")
XCTAssertNotNil(sut.createdAt)
XCTAssertNotNil(sut.updatedAt)
XCTAssertTrue(sut.items.isEmpty, "New user should have no items")
}
// Test: Valid email passes validation
func testEmailValidation_Valid() {
// Given
sut.email = "valid@example.com"
// When
let isValid = sut.isValid
// Then
XCTAssertTrue(isValid, "Valid email should pass validation")
}
// Test: Invalid email fails validation
func testEmailValidation_Invalid() {
// Given
sut.email = "invalid.com" // Missing @
// When
let isValid = sut.isValid
// Then
XCTAssertFalse(isValid, "Invalid email should fail validation")
}
// Test: Display name returns first name only
func testDisplayName_FullName() {
// Given
sut.name = "John Doe"
// When
let displayName = sut.displayName
// Then
XCTAssertEqual(displayName, "John", "Display name should be first name only")
}
// Test: Initials are correctly generated
func testInitials_FullName() {
// Given
sut.name = "John Doe"
// When
let initials = sut.initials
// Then
XCTAssertEqual(initials, "JD", "Initials should be JD")
}
// Test: Update profile changes name and timestamp
func testUpdateProfile_Name() {
// Given
let originalUpdatedAt = sut.updatedAt
sleep(1) // Ensure timestamp changes
// When
sut.updateProfile(name: "Jane Doe")
// Then
XCTAssertEqual(sut.name, "Jane Doe")
XCTAssertNotEqual(sut.updatedAt, originalUpdatedAt, "updatedAt should change")
}
// Test: Codable encoding produces valid JSON
func testCodable_Encoding() throws {
// Given: User from setUp
// When
let encoder = JSONEncoder()
let data = try encoder.encode(sut)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
// Then
XCTAssertNotNil(json)
XCTAssertEqual(json?["name"] as? String, "Test User")
XCTAssertEqual(json?["email"] as? String, "test@example.com")
XCTAssertNotNil(json?["id"])
}
}
```
**Repeat for all models**: Item, Category, etc.
---
#### Test Suite: Item Model
[Similar structure for ItemTests.swift]
**Key Test Cases**:
- Initialization
- Validation (title not empty, description not empty)
- Status updates
- Relationships (owner, category)
- Codable conformance
---
### 2.2 ViewModel Tests
Test all ViewModels from IMPLEMENTATION_GUIDE.
#### Test Suite: HomeViewModel
**File**: `[AppName]Tests/ViewModelTests/HomeViewModelTests.swift`
| Test Case | Mock Setup | Action | Expected Result | Priority |
|-----------|------------|--------|-----------------|----------|
| testInitialState | New ViewModel | None | items empty, isLoading false, showError false | P0 |
| testLoadItems_Success | Mock API returns 3 items | Call loadItems() | isLoading becomes false, items count = 3, no error | P0 |
| testLoadItems_Failure | Mock API throws error | Call loadItems() | isLoading false, showError true, errorMessage set | P0 |
| testLoadItems_EmptyResponse | Mock API returns empty array | Call loadItems() | items empty, no error | P1 |
| testRefresh | 3 items already loaded | Call refresh() | isLoading true then false, items refreshed | P1 |
| testDeleteItem_Success | 3 items loaded | Delete item[1] | items count = 2, deleted item removed | P0 |
| testDeleteItem_Failure | Mock API throws error on delete | Delete item | showError true, item not removed | P1 |
| testToggleItemStatus_Active | Item with status .active | toggleItemStatus(item) | item.status becomes .completed | P0 |
| testToggleItemStatus_Completed | Item with status .completed | toggleItemStatus(item) | item.status becomes .active | P0 |
| testSearchItems_MatchFound | 3 items, searchQuery = "Test" | Set searchQuery | items filtered to matching only | P1 |
| testSearchItems_NoMatch | 3 items, searchQuery = "XYZ" | Set searchQuery | items empty | P1 |
| testSearchItems_EmptyQuery | searchQuery was "Test" | Set searchQuery = "" | All items shown again | P1 |
| testLoadItems_LocalFirst | Mock delays API response | Call loadItems() | Local items shown immediately, then synced | P2 |
**Implementation Example**:
```swift
import XCTest
@testable import [AppName]
final class HomeViewModelTests: XCTestCase {
var sut: HomeViewModel!
var mockAPIClient: MockAPIClient!
var mockDataManager: MockDataManager!
override func setUp() {
super.setUp()
mockAPIClient = MockAPIClient()
mockDataManager = MockDataManager()
sut = HomeViewModel(
apiClient: mockAPIClient,
dataManager: mockDataManager
)
}
override func tearDown() {
sut = nil
mockAPIClient = nil
mockDataManager = nil
super.tearDown()
}
// Test: Initial state is correct
@MainActor
func testInitialState() {
// Then
XCTAssertTrue(sut.items.isEmpty)
XCTAssertFalse(sut.isLoading)
XCTAssertFalse(sut.showError)
XCTAssertTrue(sut.searchQuery.isEmpty)
}
// Test: Loading items succeeds
@MainActor
func testLoadItems_Success() async {
// Given
let mockItems = [
Item(title: "Item 1", description: "Desc 1", owner: mockUser()),
Item(title: "Item 2", description: "Desc 2", owner: mockUser()),
Item(title: "Item 3", description: "Desc 3", owner: mockUser())
]
mockDataManager.itemsToReturn = mockItems
mockAPIClient.itemsToReturn = mockItems
// When
await sut.loadItems()
// Then
XCTAssertEqual(sut.items.count, 3)
XCTAssertFalse(sut.isLoading)
XCTAssertFalse(sut.showError)
}
// Test: Loading items handles error
@MainActor
func testLoadItems_Failure() async {
// Given
mockDataManager.shouldThrowError = true
// When
await sut.loadItems()
// Then
XCTAssertTrue(sut.showError)
XCTAssertFalse(sut.errorMessage.isEmpty)
XCTAssertFalse(sut.isLoading)
}
View on GitHub