diff --git a/Package.swift b/Package.swift index 524a4db..805a6aa 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,10 @@ let package = Package( name: "tinyTCA", targets: ["tinyTCA"] ), + .library( + name: "tinyTCATestUtils", + targets: ["tinyTCATestUtils"] + ) ], targets: [ .target( @@ -22,6 +26,14 @@ let package = Package( // Strict concurrency checking (recommended for Swift 6) .enableExperimentalFeature("StrictConcurrency") ] + ), + .target( + name: "tinyTCATestUtils", + dependencies: ["tinyTCA"] + ), + .testTarget( + name: "tinyTCATests", + dependencies: ["tinyTCA", "tinyTCATestUtils"] ) ] ) diff --git a/Sources/tinyTCA/tinyTCA.swift b/Sources/tinyTCA/tinyTCA.swift index d0fa786..c1c3556 100644 --- a/Sources/tinyTCA/tinyTCA.swift +++ b/Sources/tinyTCA/tinyTCA.swift @@ -50,7 +50,6 @@ public final class Store: ObservableObject { } } - /// A two-way binding to the store's state for SwiftUI public var binding: Binding { Binding( diff --git a/Sources/tinyTCATestUtils/TestStore.swift b/Sources/tinyTCATestUtils/TestStore.swift new file mode 100644 index 0000000..83f09ca --- /dev/null +++ b/Sources/tinyTCATestUtils/TestStore.swift @@ -0,0 +1,23 @@ +import Foundation +import Combine +@testable import tinyTCA + +/// A test store for testing state changes in isolation. +public final class TestStore { + public init(state: F.State, feature: F) { + self.state = state + self.feature = feature + } + + public init(feature: F) { + self.state = feature.initialState + self.feature = feature + } + + let feature: F + public private(set) var state: F.State + + public func send(_ action: F.Action) { + try? feature.reducer(state: &state, action: action) + } +} diff --git a/Tests/tinyTCATests/tinyTCATests.swift b/Tests/tinyTCATests/tinyTCATests.swift new file mode 100644 index 0000000..5b21916 --- /dev/null +++ b/Tests/tinyTCATests/tinyTCATests.swift @@ -0,0 +1,60 @@ +@testable import tinyTCA +import tinyTCATestUtils +import Testing + +@Suite("Counter Feature Tests") +struct CounterFeatureTests { + @Test("Counter increments and decrements correctly") + func testCounter() async throws { + let testStore = TestStore(feature: CounterFeature()) + + testStore.send(.increment) + #expect(testStore.state.count == 1) + + testStore.send(.decrement) + #expect(testStore.state.count == 0) + + testStore.send(.delayedIncrement) + #expect(testStore.state.count == 0) + } +} + +struct CounterFeature: Feature { + struct State: Equatable { + var count = 0 + } + + enum Action: Equatable { + case increment + case decrement + case delayedIncrement + case delayedIncrementResponse + } + + var initialState: State { State() } + + func reducer(state: inout State, action: Action) throws { + switch action { + case .increment: + state.count += 1 + case .decrement: + state.count -= 1 + case .delayedIncrement: + // No state change, the effect handles the logic + break + case .delayedIncrementResponse: + state.count += 1 + } + } + + // not tested yet + func effect(for action: Action, state: State) async throws -> Action? { + switch action { + case .delayedIncrement: + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second + return .delayedIncrementResponse + default: + return nil + } + } +}