一键导入
luca-test
Writes unit tests for LUCA Service and Store components using Swift Testing, with proper naming conventions and dependency mocking patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Writes unit tests for LUCA Service and Store components using Swift Testing, with proper naming conventions and dependency mocking patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Explains the LUCA architecture — its three-layer structure, key components, data flow, and patterns for SwiftUI app development.
Implements features following the LUCA architecture — generates DataSource, Model, and UserInterface layer code with all coding rules applied.
Guides project scaffolding for LUCA-based Xcode projects — installing prerequisites, running the luca CLI, and understanding the generated project structure.
| name | luca-test |
| description | Writes unit tests for LUCA Service and Store components using Swift Testing, with proper naming conventions and dependency mocking patterns. |
You are an expert in writing unit tests for the LUCA architecture. When the user provides a Store or Service implementation, generate comprehensive Swift Testing tests following all LUCA test conventions below.
Ask the user to share the Store/Service code they want to test if not already provided.
| Target | Location | Test file location |
|---|---|---|
Service | Sources/Model/Services/ | Tests/ModelTests/ServiceTests/ |
Store | Sources/Model/Stores/ | Tests/ModelTests/StoreTests/ |
Do not write UI tests or snapshot tests — unit tests for Services and Stores are sufficient to cover app functionality in LUCA.
Use Swift Testing (not XCTest):
import Testing
import DataSource
@testable import Model
Mark test structs/classes with @MainActor when testing Stores (since Stores are @MainActor):
@MainActor struct FooStoreTests {
@Test
func send_task_itemsAreLoaded() async { ... }
}
func {methodOrAction}_{condition_optional}_{expectedResult}()
Store tests (Action-based):
func send_task_savedDataIsRestored()
func send_deleteButtonTapped_itemIsSelected_itemIsDeleted()
func send_notificationsToggleSwitched_notificationsAreDisabled_notificationsAreEnabled()
func send_themePickerSelected_themeIsUpdated()
func send_fetchDataResponse_success_itemsArePopulated()
func send_fetchDataResponse_failure_errorIsShown()
Service tests (method-based):
func calculateBMI_heightIsNonZero_correctValueIsReturned()
func calculateBMI_heightIsZero_zeroIsReturned()
func processInput_emptyString_emptyStringIsReturned()
testDependency(of:injection:) (single client)let client = testDependency(of: UserDefaultsClient.self) {
$0.data = { _ in
try! JSONEncoder().encode(Foo(name: "test", value: 42))
}
}
AppDependencies.testDependencies(...) (full dependency set)let deps = AppDependencies.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.data = { _ in
try! JSONEncoder().encode(Foo(name: "test", value: 42))
}
}
)
Use OSAllocatedUnfairLock to safely capture values from @Sendable closures:
import os
let captured = OSAllocatedUnfairLock<Data?>(initialState: nil)
let deps = AppDependencies.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.setData = { data, _ in
captured.withLock { $0 = data }
}
}
)
// After test action:
#expect(captured.withLock(\.self) != nil)
AppStateClient with inspectable statelet appState = OSAllocatedUnfairLock<AppState>(initialState: .init(hasAlreadyBootstrap: false))
let deps = AppDependencies.testDependencies(
appStateClient: .testDependency(appState)
)
// After test action:
#expect(appState.withLock(\.hasAlreadyBootstrap) == true)
A stateful, side-effecting dependency (UserDefaults, a file store, the keychain) does not need a
bespoke stateful fake class or a shared storage helper. Build the minimal state inline per test with
OSAllocatedUnfairLock + testDependency(of:), choosing the shape by what you verify:
$0.integer = { _ in 3 }).reduce/Service persists a value then immediately re-reads it) → back that
one key with a lock so set → get connect:let stored = OSAllocatedUnfairLock<Int>(initialState: 0)
let client = testDependency(of: UserDefaultsClient.self) {
$0.integer = { key in key == .triggerMethod ? stored.withLock(\.self) : 0 }
$0.set = { value, key in
let raw = value as? Int // cast OUTSIDE withLock — `value: Any?` is not Sendable
if key == .triggerMethod { stored.withLock { $0 = raw } }
}
}
A Repository is composed of clients, not a test seam: construct the real Repository over a mocked
Client and observe the client interaction — don't read persistence back through a second Repository
instance. For migration / transform logic (read legacy → write current → delete legacy), stub the
legacy reads and capture the set/removeObject calls; no key-value store is required.
Services are stateless structs — test them as pure functions:
import Testing
import DataSource
@testable import Model
@MainActor struct BMIServiceTests {
@Test
func calculateBMI_heightIsNonZero_correctValueIsReturned() {
let sut = BMIService(.testDependencies())
let result = sut.calculateBMI(weight: 70, height: 175)
#expect(result == 22.86)
}
@Test
func calculateBMI_heightIsZero_zeroIsReturned() {
let sut = BMIService(.testDependencies())
let result = sut.calculateBMI(weight: 70, height: 0)
#expect(result == 0)
}
}
TestStore is a testing utility generated in Tests/ModelTests/TestStore.swift. It wraps a Composable store, intercepts the parent action callback, and records all delegated actions in actionHistory.
When to use:
Initialization — always use the factory pattern:
let sut = TestStore { action in
FooStore(.testDependencies(), action: action)
}
The action closure passed to the factory is TestStore's interceptor. Any action the Store forwards to its parent via Composable.send is recorded.
Verifying delegated actions with receive:
await sut.send(.closeButtonTapped)
sut.receive {
if case .closeButtonTapped = $0 { true } else { false }
}
send dispatches the action and then removes the directly passed-through action (since Composable.send always calls action with the same value)receive verifies that a specific action appeared in actionHistory from an async internal dispatch; call it once per expected delegation before the next sendAction is not required to be Equatable — always use pattern matching (if case) inside the closureactionHistory is non-empty when send is called again, the test fails automaticallyAccessing Store properties via dynamic member lookup:
#expect(sut.items.count == 3) // proxies wrappedStore.items
@MainActor struct FooStoreTests {
@Test
func send_task_savedDataIsRestored() async {
let expected = Foo(name: "Alice", value: 42)
let sut = FooStore(
.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.data = { _ in try! JSONEncoder().encode(expected) }
}
),
action: { _ in }
)
await sut.send(.task)
#expect(sut.foo == expected)
}
}
@Test
func send_calculateButtonTapped_bmiIsCalculated() async {
let sut = FooStore(
.testDependencies(),
person: Person(name: "Bob", weight: 70.0, height: 175.0),
action: { _ in }
)
await sut.send(.calculateButtonTapped)
#expect(sut.calculatedBMI == 22.86)
}
@Test
func send_saveButtonTapped_dataIsPersisted() async {
let captured = OSAllocatedUnfairLock<Data?>(initialState: nil)
let sut = FooStore(
.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.setData = { data, _ in captured.withLock { $0 = data } }
}
),
foo: Foo(name: "Alice", value: 42),
action: { _ in }
)
await sut.send(.saveButtonTapped)
#expect(captured.withLock(\.self) != nil)
}
@Test
func send_task_tutorialIsShownOnFirstLaunch() async {
let appState = OSAllocatedUnfairLock<AppState>(initialState: .init(hasAlreadyTutorial: false))
let sut = FooStore(
.testDependencies(appStateClient: .testDependency(appState)),
action: { _ in }
)
await sut.send(.task)
#expect(sut.isPresentedTutorial == true)
#expect(appState.withLock(\.hasAlreadyTutorial) == true)
}
AsyncStreamBundle)A Service that pushes into an AppState stream is verified synchronously via latestValue — no waiting needed:
@Test
func increment_sends_incremented_count() {
let appState = OSAllocatedUnfairLock<AppState>(initialState: .init())
let sut = CounterService(.testDependencies(appStateClient: .testDependency(appState)))
sut.increment()
sut.increment()
#expect(appState.withLock(\.count.latestValue) == 2)
}
A Store that subscribes to a stream reacts asynchronously, so push a value then poll with waitUntil. Always send .onDisappear at the end to cancel the subscription.
Add this helper once at Tests/ModelTests/WaitUntil.swift:
import Foundation
@discardableResult
func waitUntil(_ condition: @MainActor () -> Bool) async -> Bool {
for _ in 0 ..< 200 {
if await condition() { return true }
try? await Task.sleep(for: .milliseconds(10))
}
return false
}
@MainActor @Test
func observes_count_stream() async {
let appState = OSAllocatedUnfairLock<AppState>(initialState: .init())
let sut = ContentStore(.testDependencies(appStateClient: .testDependency(appState)))
await sut.send(.task("Test"))
appState.withLock { $0.count.send(5) }
await waitUntil { sut.count == 5 }
#expect(sut.count == 5)
await sut.send(.onDisappear)
}
latestValue is synchronous). Consumer tests are inherently async — use waitUntil, never assert immediately after a send..task (establish subscription) then the producing Action, then waitUntil on the resulting state.When testing that a Store correctly delegates to its parent via action, use TestStore:
@Test
func send_closeButtonTapped_parentReceivesAction() async {
let sut = TestStore { action in
ChildStore(action: action)
}
await sut.send(.closeButtonTapped)
sut.receive {
if case .closeButtonTapped = $0 { true } else { false }
}
}
action: { _ in } — always pass a no-op action closure when the parent delegation is not under testinit parameters for precondition setup (avoid reaching in and mutating after init)OSAllocatedUnfairLock for @Sendable capture — never use var captured in closures directly@testable import Model is required to access internal types in testsGenerate complete, compilable test files with proper imports. Cover both happy-path and edge-case scenarios. Name every test following the convention above.