mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
* wip * wip * Update Testing.md * wip * wip * wip * wip * wip: * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * disable library evolution * bump * wip --------- Co-authored-by: Brandon Williams <mbrandonw@hey.com>
324 lines
8.9 KiB
Swift
324 lines
8.9 KiB
Swift
import ComposableArchitecture
|
||
import XCTest
|
||
|
||
final class TestStoreFailureTests: BaseTCATestCase {
|
||
@MainActor
|
||
func testNoStateChangeFailure() async {
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { state, action in
|
||
switch action {
|
||
case .first: return .send(.second)
|
||
case .second: return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - Expected state to change, but no change occurred.
|
||
|
||
The trailing closure made no observable modifications to state. If no change to state is \
|
||
expected, omit the trailing closure.
|
||
"""
|
||
}
|
||
await store.send(.first) { _ = $0 }
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - Expected state to change, but no change occurred.
|
||
|
||
The trailing closure made no observable modifications to state. If no change to state is \
|
||
expected, omit the trailing closure.
|
||
"""
|
||
}
|
||
await store.receive(.second) { _ = $0 }
|
||
}
|
||
|
||
@MainActor
|
||
func testStateChangeFailure() async {
|
||
struct State: Equatable { var count = 0 }
|
||
let store = TestStore(initialState: State()) {
|
||
Reduce<State, Void> { state, action in
|
||
state.count += 1
|
||
return .none
|
||
}
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - A state change does not match expectation: …
|
||
|
||
− TestStoreFailureTests.State(count: 0)
|
||
+ TestStoreFailureTests.State(count: 1)
|
||
|
||
(Expected: −, Actual: +)
|
||
"""
|
||
}
|
||
await store.send(()) { $0.count = 0 }
|
||
}
|
||
|
||
@MainActor
|
||
func testUnexpectedStateChangeOnSendFailure() async {
|
||
struct State: Equatable { var count = 0 }
|
||
let store = TestStore(initialState: State()) {
|
||
Reduce<State, Void> { state, action in
|
||
state.count += 1
|
||
return .none
|
||
}
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - State was not expected to change, but a change occurred: …
|
||
|
||
− TestStoreFailureTests.State(count: 0)
|
||
+ TestStoreFailureTests.State(count: 1)
|
||
|
||
(Expected: −, Actual: +)
|
||
"""
|
||
}
|
||
await store.send(())
|
||
}
|
||
|
||
@MainActor
|
||
func testUnexpectedStateChangeOnReceiveFailure() async {
|
||
struct State: Equatable { var count = 0 }
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: State()) {
|
||
Reduce<State, Action> { state, action in
|
||
switch action {
|
||
case .first: return .send(.second)
|
||
case .second:
|
||
state.count += 1
|
||
return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
await store.send(.first)
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - State was not expected to change, but a change occurred: …
|
||
|
||
− TestStoreFailureTests.State(count: 0)
|
||
+ TestStoreFailureTests.State(count: 1)
|
||
|
||
(Expected: −, Actual: +)
|
||
"""
|
||
}
|
||
await store.receive(.second)
|
||
}
|
||
|
||
@MainActor
|
||
func testReceivedActionAfterDeinit() async {
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { state, action in
|
||
switch action {
|
||
case .first: return .send(.second)
|
||
case .second: return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - The store received 1 unexpected action: …
|
||
|
||
Unhandled actions:
|
||
• .second
|
||
|
||
To fix, explicitly assert against these actions using "store.receive", skip these actions \
|
||
by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \
|
||
store: "store.exhaustivity = .off".
|
||
"""
|
||
}
|
||
await store.send(.first)
|
||
}
|
||
|
||
@MainActor
|
||
func testReceivedActionBeforeFinish() async {
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { state, action in
|
||
switch action {
|
||
case .first: return .send(.second)
|
||
case .second: return .none
|
||
}
|
||
}
|
||
}
|
||
self.longLivingStore = store
|
||
|
||
await store.send(.first)
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - The store received 1 unexpected action: …
|
||
|
||
Unhandled actions:
|
||
• .second
|
||
|
||
To fix, explicitly assert against these actions using "store.receive", skip these actions \
|
||
by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \
|
||
store: "store.exhaustivity = .off".
|
||
"""
|
||
}
|
||
await store.finish()
|
||
}
|
||
private var longLivingStore: AnyObject?
|
||
|
||
@MainActor
|
||
func testEffectInFlightAfterDeinit() async {
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Void> { state, action in
|
||
.run { _ in try await Task.never() }
|
||
}
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - An effect returned for this action is still running. It must complete before the \
|
||
end of the test. …
|
||
|
||
To fix, inspect any effects the reducer returns for this action and ensure that all of \
|
||
them complete by the end of the test. There are a few reasons why an effect may not have \
|
||
completed:
|
||
|
||
• If using async/await in your effect, it may need a little bit of time to properly \
|
||
finish. To fix you can simply perform "await store.finish()" at the end of your test.
|
||
|
||
• If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", etc.), \
|
||
make sure that you wait enough time for it to perform the effect. If you are using a test \
|
||
clock/scheduler, advance it so that the effects may complete, or consider using an \
|
||
immediate clock/scheduler to immediately perform the effect instead.
|
||
|
||
• If you are returning a long-living effect (timers, notifications, subjects, etc.), then \
|
||
make sure those effects are torn down by marking the effect ".cancellable" and returning a \
|
||
corresponding cancellation effect ("Effect.cancel") from another action, or, if your \
|
||
effect is driven by a Combine subject, send it a completion.
|
||
|
||
• If you do not wish to assert on these effects, perform "await \
|
||
store.skipInFlightEffects()", or consider using a non-exhaustive test store: \
|
||
"store.exhaustivity = .off".
|
||
"""
|
||
}
|
||
await store.send(())
|
||
}
|
||
|
||
@MainActor
|
||
func testSendActionBeforeReceivingFailure() async {
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { state, action in
|
||
switch action {
|
||
case .first: return .send(.second)
|
||
case .second: return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
await store.send(.first)
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - Must handle 1 received action before sending an action: …
|
||
|
||
Unhandled actions: [
|
||
[0]: .second
|
||
]
|
||
"""
|
||
}
|
||
await store.send(.first)
|
||
|
||
await store.receive(.second)
|
||
await store.receive(.second)
|
||
}
|
||
|
||
@MainActor
|
||
func testReceiveNonExistentActionFailure() async {
|
||
enum Action { case action }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { _, _ in .none }
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - Expected to receive the following action, but didn't: …
|
||
|
||
TestStoreFailureTests.Action.action
|
||
"""
|
||
}
|
||
await store.receive(.action)
|
||
}
|
||
|
||
@MainActor
|
||
func testReceiveUnexpectedActionFailure() async {
|
||
enum Action { case first, second }
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Action> { state, action in
|
||
switch action {
|
||
case .first:
|
||
return .send(.second)
|
||
case .second:
|
||
state += 1
|
||
return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
await store.send(.first)
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == """
|
||
failed - Received unexpected action: …
|
||
|
||
− TestStoreFailureTests.Action.first
|
||
+ TestStoreFailureTests.Action.second
|
||
|
||
(Expected: −, Received: +)
|
||
"""
|
||
}
|
||
await store.receive(.first)
|
||
}
|
||
|
||
@MainActor
|
||
func testModifyClosureThrowsErrorFailure() async {
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Void> { _, _ in .none }
|
||
}
|
||
|
||
XCTExpectFailure {
|
||
$0.compactDescription == "failed - Threw error: SomeError()"
|
||
}
|
||
await store.send(()) { _ in
|
||
struct SomeError: Error {}
|
||
throw SomeError()
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
func testExpectedStateEqualityMustModify() async {
|
||
let store = TestStore(initialState: 0) {
|
||
Reduce<Int, Bool> { state, action in
|
||
switch action {
|
||
case true: return .send(false)
|
||
case false: return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
await store.send(true)
|
||
await store.receive(false)
|
||
|
||
XCTExpectFailure()
|
||
await store.send(true) {
|
||
$0 = 0
|
||
}
|
||
|
||
XCTExpectFailure()
|
||
await store.receive(false) {
|
||
$0 = 0
|
||
}
|
||
}
|
||
}
|