Files
daymade 4e3a54175e Release v1.18.0: Add iOS-APP-developer and promptfoo-evaluation skills
### Added
- **New Skill**: iOS-APP-developer v1.1.0 - iOS development with XcodeGen, SwiftUI, and SPM
  - XcodeGen project.yml configuration
  - SPM dependency resolution
  - Device deployment and code signing
  - Camera/AVFoundation debugging
  - iOS version compatibility handling
  - Library not loaded @rpath framework error fixes
  - State machine testing patterns for @MainActor classes
  - Bundled references: xcodegen-full.md, camera-avfoundation.md, swiftui-compatibility.md, testing-mainactor.md

- **New Skill**: promptfoo-evaluation v1.0.0 - LLM evaluation framework using Promptfoo
  - Promptfoo configuration (promptfooconfig.yaml)
  - Python custom assertions
  - llm-rubric for LLM-as-judge evaluations
  - Few-shot example management
  - Model comparison and prompt testing
  - Bundled reference: promptfoo_api.md

### Changed
- Updated marketplace version from 1.16.0 to 1.18.0
- Updated marketplace skills count from 23 to 25
- Updated skill-creator to v1.2.2:
  - Fixed best practices documentation URL (platform.claude.com)
  - Enhanced quick_validate.py to exclude file:// prefixed paths from validation
- Updated marketplace.json metadata description to include new skills

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 17:23:08 +08:00

147 lines
3.8 KiB
Markdown

# Testing @MainActor Classes
## The Problem
Testing `@MainActor` classes like `ObservableObject` controllers in Swift 6 is challenging because:
1. `setUp()` and `tearDown()` are nonisolated
2. Properties with `private(set)` can't be set from tests
3. Direct property access from tests triggers concurrency errors
## Solution: setStateForTesting Pattern
Add a DEBUG-only method to allow tests to set internal state:
```swift
@MainActor
final class TrainingSessionController: ObservableObject {
@Published private(set) var state: TrainingState = .idle
// ... rest of controller ...
// MARK: - Testing Support
#if DEBUG
/// Set state directly for testing purposes only
func setStateForTesting(_ newState: TrainingState) {
state = newState
}
#endif
}
```
## Test Class Structure
```swift
import XCTest
@testable import YourApp
@MainActor
final class TrainingSessionControllerTests: XCTestCase {
var controller: TrainingSessionController!
override func setUp() {
super.setUp()
controller = TrainingSessionController()
}
override func tearDown() {
controller = nil
super.tearDown()
}
func testConfigureFromFailedStateAutoResets() {
// Arrange: Set to failed state
controller.setStateForTesting(.failed("Recording too short"))
// Act: Configure should recover
controller.configure(with: PhaseConfig.default)
// Assert: Should be back in idle
XCTAssertEqual(controller.state, .idle)
}
}
```
## State Machine Testing Patterns
### Testing State Transitions
```swift
func testStateTransitions() {
// Test each state's behavior
let states: [TrainingState] = [.idle, .completed, .failed("error")]
for state in states {
controller.setStateForTesting(state)
controller.configure(with: PhaseConfig.default)
// Verify expected outcome
XCTAssertTrue(controller.canStart, "\(state) should allow starting")
}
}
```
### Regression Tests
For bugs that have been fixed, add specific regression tests:
```swift
/// Regression test for: State machine dead-lock after recording failure
/// Bug: After error, controller stayed in failed state forever
func testRegressionFailedStateDeadLock() {
// Simulate the bug scenario
controller.configure(with: PhaseConfig.default)
controller.setStateForTesting(.failed("录音太短"))
// The fix: configure() should auto-reset from failed state
controller.configure(with: PhaseConfig.default)
XCTAssertEqual(controller.state, .idle,
"REGRESSION: Failed state should not block configure()")
}
```
### State Machine Invariants
Test invariants that should always hold:
```swift
/// No terminal state should become a "dead end"
func testAllTerminalStatesAreRecoverable() {
let terminalStates: [TrainingState] = [
.idle,
.completed,
.failed("test error")
]
for state in terminalStates {
controller.setStateForTesting(state)
// Action that should recover
controller.configure(with: PhaseConfig.default)
// Verify recovery
XCTAssertTrue(canConfigure(),
"\(state) should be recoverable via configure()")
}
}
```
## Why This Pattern Works
1. **`#if DEBUG`**: Method only exists in test builds, zero production overhead
2. **Explicit method**: Makes test-only state manipulation obvious and searchable
3. **MainActor compatible**: Method is part of the @MainActor class
4. **Swift 6 safe**: Avoids concurrency errors by staying on the main actor
## Alternative: Internal Setter
If you prefer, you can use `internal(set)` instead of `private(set)`:
```swift
@Published internal(set) var state: TrainingState = .idle
```
However, this is redundant since properties are already internal by default. The `setStateForTesting()` pattern is more explicit about test-only intent.