Skip to content

Commit ba270a8

Browse files
stephencelisp4checo
authored andcommitted
Deprecate TestStore.init that doesn't require equatable state (#1857)
And introduce `TestStore.init(initialState:reducer:observe:send:)` for testing scoped state and actions. (cherry picked from commit 0ed5c83d9608518f8ea6ae757b58f42f33ad407f)
1 parent 90ea5d4 commit ba270a8

File tree

5 files changed

+169
-18
lines changed

5 files changed

+169
-18
lines changed

Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ final class GameSwiftUITests: XCTestCase {
1111
oPlayerName: "Blob Jr.",
1212
xPlayerName: "Blob Sr."
1313
),
14-
reducer: Game()
14+
reducer: Game(),
15+
observe: GameView.ViewState.init,
16+
send: { $0 }
1517
)
16-
.scope(state: GameView.ViewState.init)
1718

1819
func testFlow_Winner_Quit() async {
1920
await self.store.send(.cellTapped(row: 0, column: 0)) {

Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ final class LoginSwiftUITests: XCTestCase {
1010
func testFlow_Success() async {
1111
let store = TestStore(
1212
initialState: Login.State(),
13-
reducer: Login()
13+
reducer: Login(),
14+
observe: LoginView.ViewState.init,
15+
send: action: Login.Action.init
1416
) {
1517
$0.authenticationClient.login = { _ in
1618
AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
1719
}
1820
}
19-
.scope(state: LoginView.ViewState.init, action: Login.Action.init)
2021

2122
await store.send(.emailChanged("[email protected]")) {
2223
$0.email = "[email protected]"
@@ -42,13 +43,14 @@ final class LoginSwiftUITests: XCTestCase {
4243
func testFlow_Success_TwoFactor() async {
4344
let store = TestStore(
4445
initialState: Login.State(),
45-
reducer: Login()
46+
reducer: Login(),
47+
observe: LoginView.ViewState.init,
48+
send: action: Login.Action.init
4649
) {
4750
$0.authenticationClient.login = { _ in
4851
AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
4952
}
5053
}
51-
.scope(state: LoginView.ViewState.init, action: Login.Action.init)
5254

5355
await store.send(.emailChanged("[email protected]")) {
5456
$0.email = "[email protected]"
@@ -78,13 +80,14 @@ final class LoginSwiftUITests: XCTestCase {
7880
func testFlow_Failure() async {
7981
let store = TestStore(
8082
initialState: Login.State(),
81-
reducer: Login()
83+
reducer: Login(),
84+
observe: LoginView.ViewState.init,
85+
send: action: Login.Action.init
8286
) {
8387
$0.authenticationClient.login = { _ in
8488
throw AuthenticationError.invalidUserPassword
8589
}
8690
}
87-
.scope(state: LoginView.ViewState.init, action: Login.Action.init)
8891

8992
await store.send(.emailChanged("blob")) {
9093
$0.email = "blob"

Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import XCTest
88
final class NewGameSwiftUITests: XCTestCase {
99
let store = TestStore(
1010
initialState: NewGame.State(),
11-
reducer: NewGame()
11+
reducer: NewGame(),
12+
observe: NewGameView.ViewState.init,
13+
send: NewGame.Action.init
1214
)
13-
.scope(state: NewGameView.ViewState.init, action: NewGame.Action.init)
1415

1516
func testNewGame() async {
1617
await self.store.send(.xPlayerNameChanged("Blob Sr.")) {

Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ final class TwoFactorSwiftUITests: XCTestCase {
1010
func testFlow_Success() async {
1111
let store = TestStore(
1212
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
13-
reducer: TwoFactor()
13+
reducer: TwoFactor(),
14+
observe: TwoFactorView.ViewState.init,
15+
send: TwoFactor.Action.init
1416
) {
1517
$0.authenticationClient.twoFactor = { _ in
1618
AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
1719
}
1820
}
19-
.scope(state: TwoFactorView.ViewState.init, action: TwoFactor.Action.init)
2021

2122
await store.send(.codeChanged("1")) {
2223
$0.code = "1"
@@ -50,13 +51,14 @@ final class TwoFactorSwiftUITests: XCTestCase {
5051
func testFlow_Failure() async {
5152
let store = TestStore(
5253
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
53-
reducer: TwoFactor()
54+
reducer: TwoFactor(),
55+
observe: TwoFactorView.ViewState.init,
56+
send: TwoFactor.Action.init
5457
) {
5558
$0.authenticationClient.twoFactor = { _ in
5659
throw AuthenticationError.invalidTwoFactor
5760
}
5861
}
59-
.scope(state: TwoFactorView.ViewState.init, action: TwoFactor.Action.init)
6062

6163
await store.send(.codeChanged("1234")) {
6264
$0.code = "1234"

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,16 +567,146 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
567567
/// - Parameters:
568568
/// - initialState: The state the feature starts in.
569569
/// - reducer: The reducer that powers the runtime of the feature.
570-
public init<Reducer: ReducerProtocol>(
570+
/// - prepareDependencies: A closure that can be used to override dependencies that will be
571+
/// accessed during the test. These dependencies will be used when producing the initial
572+
/// state.
573+
public convenience init<R: ReducerProtocol>(
571574
initialState: @autoclosure () -> State,
572-
reducer: Reducer,
575+
reducer: R,
573576
prepareDependencies: (inout DependencyValues) -> Void = { _ in },
574577
file: StaticString = #file,
575578
line: UInt = #line
576579
)
577580
where
578-
Reducer.State == State,
579-
Reducer.Action == Action,
581+
R.State == State,
582+
R.Action == Action,
583+
State == ScopedState,
584+
State: Equatable,
585+
Action == ScopedAction,
586+
Environment == Void
587+
{
588+
self.init(
589+
initialState: initialState(),
590+
reducer: reducer,
591+
observe: { $0 },
592+
send: { $0 },
593+
prepareDependencies: prepareDependencies,
594+
file: file,
595+
line: line
596+
)
597+
}
598+
599+
/// Creates a scoped test store with an initial state and a reducer powering its runtime.
600+
///
601+
/// See <doc:Testing> and the documentation of ``TestStore`` for more information on how to best
602+
/// use a test store.
603+
///
604+
/// - Parameters:
605+
/// - initialState: The state the feature starts in.
606+
/// - reducer: The reducer that powers the runtime of the feature.
607+
/// - toScopedState: A function that transforms the reducer's state into scoped state. This
608+
/// state will be asserted against as it is mutated by the reducer. Useful for testing view
609+
/// store state transformations.
610+
/// - prepareDependencies: A closure that can be used to override dependencies that will be
611+
/// accessed during the test. These dependencies will be used when producing the initial
612+
/// state.
613+
public convenience init<R: ReducerProtocol>(
614+
initialState: @autoclosure () -> State,
615+
reducer: R,
616+
observe toScopedState: @escaping (State) -> ScopedState,
617+
prepareDependencies: (inout DependencyValues) -> Void = { _ in },
618+
file: StaticString = #file,
619+
line: UInt = #line
620+
)
621+
where
622+
R.State == State,
623+
R.Action == Action,
624+
ScopedState: Equatable,
625+
Action == ScopedAction,
626+
Environment == Void
627+
{
628+
self.init(
629+
initialState: initialState(),
630+
reducer: reducer,
631+
observe: toScopedState,
632+
send: { $0 },
633+
prepareDependencies: prepareDependencies,
634+
file: file,
635+
line: line
636+
)
637+
}
638+
639+
/// Creates a scoped test store with an initial state and a reducer powering its runtime.
640+
///
641+
/// See <doc:Testing> and the documentation of ``TestStore`` for more information on how to best
642+
/// use a test store.
643+
///
644+
/// - Parameters:
645+
/// - initialState: The state the feature starts in.
646+
/// - reducer: The reducer that powers the runtime of the feature.
647+
/// - toScopedState: A function that transforms the reducer's state into scoped state. This
648+
/// state will be asserted against as it is mutated by the reducer. Useful for testing view
649+
/// store state transformations.
650+
/// - fromScopedAction: A function that wraps a more scoped action in the reducer's action.
651+
/// Scoped actions can be "sent" to the store, while any reducer action may be received.
652+
/// Useful for testing view store action transformations.
653+
/// - prepareDependencies: A closure that can be used to override dependencies that will be
654+
/// accessed during the test. These dependencies will be used when producing the initial
655+
/// state.
656+
public init<R: ReducerProtocol>(
657+
initialState: @autoclosure () -> State,
658+
reducer: R,
659+
observe toScopedState: @escaping (State) -> ScopedState,
660+
send fromScopedAction: @escaping (ScopedAction) -> Action,
661+
prepareDependencies: (inout DependencyValues) -> Void = { _ in },
662+
file: StaticString = #file,
663+
line: UInt = #line
664+
)
665+
where
666+
R.State == State,
667+
R.Action == Action,
668+
ScopedState: Equatable,
669+
Environment == Void
670+
{
671+
var dependencies = DependencyValues._current
672+
prepareDependencies(&dependencies)
673+
let initialState = withDependencies {
674+
$0 = dependencies
675+
} operation: {
676+
initialState()
677+
}
678+
679+
let reducer = TestReducer(Reduce(reducer), initialState: initialState)
680+
self._environment = .init(wrappedValue: ())
681+
self.file = file
682+
self.fromScopedAction = fromScopedAction
683+
self.line = line
684+
self.reducer = reducer
685+
self.store = Store(initialState: initialState, reducer: reducer)
686+
self.timeout = 100 * NSEC_PER_MSEC
687+
self.toScopedState = toScopedState
688+
self.dependencies = dependencies
689+
}
690+
691+
/// Creates a test store with an initial state and a reducer powering its runtime.
692+
///
693+
/// See <doc:Testing> and the documentation of ``TestStore`` for more information on how to best
694+
/// use a test store.
695+
///
696+
/// - Parameters:
697+
/// - initialState: The state the feature starts in.
698+
/// - reducer: The reducer that powers the runtime of the feature.
699+
@available(*, deprecated, message: "State must be equatable to perform assertions.")
700+
public init<R: ReducerProtocol>(
701+
initialState: @autoclosure () -> State,
702+
reducer: R,
703+
prepareDependencies: (inout DependencyValues) -> Void = { _ in },
704+
file: StaticString = #file,
705+
line: UInt = #line
706+
)
707+
where
708+
R.State == State,
709+
R.Action == Action,
580710
State == ScopedState,
581711
Action == ScopedAction,
582712
Environment == Void
@@ -1751,6 +1881,13 @@ extension TestStore {
17511881
/// - fromScopedAction: A function that wraps a more scoped action in the reducer's action.
17521882
/// Scoped actions can be "sent" to the store, while any reducer action may be received.
17531883
/// Useful for testing view store action transformations.
1884+
@available(
1885+
*,
1886+
deprecated,
1887+
message: """
1888+
Use 'TestStore.init(initialState:reducer:observe:send:)' to scope a test store's state and actions.
1889+
"""
1890+
)
17541891
public func scope<S, A>(
17551892
state toScopedState: @escaping (ScopedState) -> S,
17561893
action fromScopedAction: @escaping (A) -> ScopedAction
@@ -1774,6 +1911,13 @@ extension TestStore {
17741911
/// - Parameter toScopedState: A function that transforms the reducer's state into scoped state.
17751912
/// This state will be asserted against as it is mutated by the reducer. Useful for testing view
17761913
/// store state transformations.
1914+
@available(
1915+
*,
1916+
deprecated,
1917+
message: """
1918+
Use 'TestStore.init(initialState:reducer:observe:)' to scope a test store's state.
1919+
"""
1920+
)
17771921
public func scope<S>(
17781922
state toScopedState: @escaping (ScopedState) -> S
17791923
) -> TestStore<State, Action, S, ScopedAction, Environment> {

0 commit comments

Comments
 (0)