diff --git a/Sources/RxComposableArchitecture/TestSupport/TestStore.swift b/Sources/RxComposableArchitecture/TestSupport/TestStore.swift index 445fd34..0ca255a 100644 --- a/Sources/RxComposableArchitecture/TestSupport/TestStore.swift +++ b/Sources/RxComposableArchitecture/TestSupport/TestStore.swift @@ -7,9 +7,9 @@ import CustomDump /// A testable runtime for a reducer. /// /// This object aids in writing expressive and exhaustive tests for features built in the -/// Composable Architecture. It allows you to send a sequence of actions to the store, and each -/// step of the way you must assert exactly how state changed, and how effect emissions were fed -/// back into the system. +/// Composable Architecture. It allows you to send a sequence of actions to the store, and each step +/// of the way you must assert exactly how state changed, and how effect emissions were fed back +/// into the system. /// /// See the dedicated article for detailed information on testing. /// @@ -19,28 +19,27 @@ import CustomDump /// sending use actions and receiving actions from effects. There are multiple ways the test store /// forces you to do this: /// -/// * After each action is sent you must describe precisely how the state changed from before -/// the action was sent to after it was sent. +/// * After each action is sent you must describe precisely how the state changed from before the +/// action was sent to after it was sent. /// -/// If even the smallest piece of data differs the test will fail. This guarantees that you -/// are proving you know precisely how the state of the system changes. +/// If even the smallest piece of data differs the test will fail. This guarantees that you are +/// proving you know precisely how the state of the system changes. /// -/// * Sending an action can sometimes cause an effect to be executed, and if that effect sends -/// an action back into the system, you **must** explicitly assert that you expect to receive -/// that action from the effect, _and_ you must assert how state changed as a result. +/// * Sending an action can sometimes cause an effect to be executed, and if that effect sends an +/// action back into the system, you **must** explicitly assert that you expect to receive that +/// action from the effect, _and_ you must assert how state changed as a result. /// -/// If you try to send another action before you have handled all effect actions, the -/// test will fail. This guarantees that you do not accidentally forget about an effect -/// action, and that the sequence of steps you are describing will mimic how the application -/// behaves in reality. +/// If you try to send another action before you have handled all effect actions, the test will +/// fail. This guarantees that you do not accidentally forget about an effect action, and that +/// the sequence of steps you are describing will mimic how the application behaves in reality. /// /// * All effects must complete by the time the test case has finished running, and all effect /// actions must be asserted on. /// /// If at the end of the assertion there is still an in-flight effect running or an unreceived /// action, the assertion will fail. This helps exhaustively prove that you know what effects -/// are in flight and forces you to prove that effects will not cause any future changes to -/// your state. +/// are in flight and forces you to prove that effects will not cause any future changes to your +/// state. /// /// For example, given a simple counter reducer: /// @@ -93,11 +92,11 @@ import CustomDump /// ``` /// /// Note that in the trailing closure of `.send(.incrementButtonTapped)` we are given a single -/// mutable value of the state before the action was sent, and it is our job to mutate the value -/// to match the state after the action was sent. In this case the `count` field changes to `1`. +/// mutable value of the state before the action was sent, and it is our job to mutate the value to +/// match the state after the action was sent. In this case the `count` field changes to `1`. /// -/// If the change made in the closure does not reflect reality, you will get a test failure with -/// a nicely formatted failure message letting you know exactly what went wrong: +/// If the change made in the closure does not reflect reality, you will get a test failure with a +/// nicely formatted failure message letting you know exactly what went wrong: /// /// ```swift /// await store.send(.incrementButtonTapped) { @@ -116,8 +115,8 @@ import CustomDump /// (Expected: −, Actual: +) /// ``` /// -/// For a more complex example, consider the following bare-bones search feature that uses a -/// clock and cancel token to debounce requests: +/// For a more complex example, consider the following bare-bones search feature that uses a clock +/// and cancel token to debounce requests: /// /// ```swift /// struct Search: ReducerProtocol { @@ -224,8 +223,8 @@ import CustomDump /// ] /// ``` /// -/// All of these types of failures help you prove that you know exactly how your feature evolves -/// as actions are sent into the system. If the library did not produce a test failure in these +/// All of these types of failures help you prove that you know exactly how your feature evolves as +/// actions are sent into the system. If the library did not produce a test failure in these /// situations it could be hiding subtle bugs in your code. For example, when the user clears the /// search query you probably expect that the results are cleared and no search request is executed /// since there is no query. This can be done like so: @@ -241,14 +240,14 @@ import CustomDump /// ``` /// /// But, if in the future a bug is introduced causing a search request to be executed even when the -/// query is empty, you will get a test failure because a new effect is being created that is -/// not being asserted on. This is the power of exhaustive testing. +/// query is empty, you will get a test failure because a new effect is being created that is not +/// being asserted on. This is the power of exhaustive testing. /// /// ## Non-exhaustive testing /// -/// While exhaustive testing can be powerful, it can also be a nuisance, especially when testing -/// how many features integrate together. This is why sometimes you may want to selectively test -/// in a non-exhaustive style. +/// While exhaustive testing can be powerful, it can also be a nuisance, especially when testing how +/// many features integrate together. This is why sometimes you may want to selectively test in a +/// non-exhaustive style. /// /// > Tip: The concept of "non-exhaustive test store" was first introduced by /// [Krzysztof Zabłocki][merowing.info] in a [blog post][exhaustive-testing-in-tca] and @@ -366,8 +365,8 @@ import CustomDump /// activity. Now the login feature is free to make any change it wants to make without affecting /// this integration test. /// -/// Using ``Exhaustivity/off`` for ``TestStore/exhaustivity`` causes all un-asserted changes to -/// pass without any notification. If you would like to see what test failures are being suppressed +/// Using ``Exhaustivity/off`` for ``TestStore/exhaustivity`` causes all un-asserted changes to pass +/// without any notification. If you would like to see what test failures are being suppressed /// without actually causing a failure, you can use ``Exhaustivity/off(showSkippedAssertions:)``: /// /// ```swift @@ -427,13 +426,12 @@ public final class TestStore @@ -559,7 +557,7 @@ public final class TestStore and the documentation of ``TestStore`` for more information on how to best /// use a test store. @@ -839,9 +837,9 @@ extension TestStore where ScopedState: Equatable { /// } /// ``` /// - /// This method suspends in order to allow any effects to start. For example, if you - /// track an analytics event in a ``EffectPublisher/fireAndForget(priority:_:)`` when an action is - /// sent, you can assert on that behavior immediately after awaiting `store.send`: + /// This method suspends in order to allow any effects to start. For example, if you track an + /// analytics event in a ``EffectPublisher/fireAndForget(priority:_:)`` when an action is sent, + /// you can assert on that behavior immediately after awaiting `store.send`: /// /// ```swift /// @MainActor @@ -865,8 +863,8 @@ extension TestStore where ScopedState: Equatable { /// } /// ``` /// - /// This method suspends only for the duration until the effect _starts_ from sending the - /// action. It does _not_ suspend for the duration of the effect. + /// This method suspends only for the duration until the effect _starts_ from sending the action. + /// It does _not_ suspend for the duration of the effect. /// /// In order to suspend for the duration of the effect you can use its return value, a /// ``TestStoreTask``, which represents the lifecycle of the effect started from sending an @@ -961,10 +959,10 @@ extension TestStore where ScopedState: Equatable { /// Sends an action to the store and asserts when state changes. /// - /// This method returns a ``TestStoreTask``, which represents the lifecycle of the effect - /// started from sending an action. You can use this value to force the cancellation of the - /// effect, which is helpful for effects that are tied to a view's lifecycle and not torn - /// down when an action is sent, such as actions sent in SwiftUI's `task` view modifier. + /// This method returns a ``TestStoreTask``, which represents the lifecycle of the effect started + /// from sending an action. You can use this value to force the cancellation of the effect, which + /// is helpful for effects that are tied to a view's lifecycle and not torn down when an action is + /// sent, such as actions sent in SwiftUI's `task` view modifier. /// /// For example, if your feature kicks off a long-living effect when the view appears by using /// SwiftUI's `task` view modifier, then you can write a test for such a feature by explicitly @@ -1176,6 +1174,8 @@ extension TestStore where ScopedState: Equatable { extension TestStore where ScopedState: Equatable, Action: Equatable { /// Asserts an action was received from an effect and asserts when state changes. /// + /// See ``receive(_:timeout:assert:file:line:)-1rwdd`` for more information of how to use this + /// /// - Parameters: /// - expectedAction: An action expected from an effect. /// - updateExpectingResult: A closure that asserts state changed by sending the action to the @@ -1215,7 +1215,7 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { } /// Asserts a matching action was received from an effect and asserts how the state changes. /// - /// See ``receive(_:timeout:assert:file:line:)-6b3xi`` for more information of how to use this + /// See ``receive(_:timeout:assert:file:line:)-3myco`` for more information of how to use this /// method. /// /// - Parameters: @@ -1252,7 +1252,7 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { /// Asserts an action was received matching a case path and asserts how the state changes. /// - /// See ``receive(_:timeout:assert:file:line:)-5n755`` for more information of how to use this + /// See ``receive(_:timeout:assert:file:line:)-4e4m0`` for more information of how to use this /// method. /// /// - Parameters: @@ -1290,21 +1290,21 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { #if swift(>=5.7) && !os(macOS) && !targetEnvironment(macCatalyst) /// Asserts an action was received from an effect and asserts how the state changes. /// - /// When an effect is executed in your feature and sends an action back into the system, you - /// can use this method to assert that fact, and further assert how state changes after the - /// effect action is received: + /// When an effect is executed in your feature and sends an action back into the system, you can + /// use this method to assert that fact, and further assert how state changes after the effect + /// action is received: /// /// ```swift - /// await store.send(.buttontTapped) + /// await store.send(.buttonTapped) /// await store.receive(.response(.success(42)) { /// $0.count = 42 /// } /// ``` /// - /// Due to the variability of concurrency in Swift, sometimes a small amount of time needs - /// to pass before effects execute and send actions, and that is why this method suspends. - /// The default time waited is very small, and typically it is enough so you should be - /// controlling your dependencies so that they do not wait for real world time to pass (see + /// Due to the variability of concurrency in Swift, sometimes a small amount of time needs to + /// pass before effects execute and send actions, and that is why this method suspends. The + /// default time waited is very small, and typically it is enough so you should be controlling + /// your dependencies so that they do not wait for real world time to pass (see /// for more information on how to do that). /// /// To change the amount of time this method waits for an action, pass an explicit `timeout` @@ -1335,34 +1335,33 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { ) } - /// Asserts an action was received from an effect that matches a predicate, and asserts how - /// the state changes. + /// Asserts an action was received from an effect that matches a predicate, and asserts how the + /// state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-5n755``, except it allows - /// you to assert that an action was received that matches a predicate without asserting - /// on all the data in the action: + /// This method is similar to ``receive(_:timeout:assert:file:line:)-4he05``, except it allows + /// you to assert that an action was received that matches a predicate without asserting on all + /// the data in the action: /// /// ```swift /// await store.send(.buttonTapped) /// await store.receive { - /// guard case .response(.suceess) = $0 else { return false } + /// guard case .response(.success) = $0 else { return false } /// return true /// } assert: { /// store.count = 42 /// } /// ``` /// - /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a - /// grey information box will show next to the `store.receive` line in Xcode letting you know - /// what data was in the effect that you chose not to assert on. + /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a grey + /// information box will show next to the `store.receive` line in Xcode letting you know what + /// data was in the effect that you chose not to assert on. /// - /// If you only want to check that a particular action case was received, then you might - /// find the ``receive(_:timeout:assert:file:line:)-5n755`` overload of this method more - /// useful. + /// If you only want to check that a particular action case was received, then you might find + /// the ``receive(_:timeout:assert:file:line:)-4he05`` overload of this method more useful. /// /// - Parameters: - /// - matchingAction: A closure that attempts to extract a value from an action. If it returns - /// `nil`, a test failure is reported. + /// - isMatching: A closure that attempts to match an action. If it returns `false`, a test + /// failure is reported. /// - duration: The amount of time to wait for the expected action. /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action /// to the store. The mutable state sent to this closure must be modified to match the state @@ -1390,8 +1389,25 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { /// Asserts an action was received from an effect and asserts how the state changes. /// - /// See ``receive(_:timeout:assert:file:line:)-332q2`` for more information on how to use this - /// method. + /// When an effect is executed in your feature and sends an action back into the system, you can + /// use this method to assert that fact, and further assert how state changes after the effect + /// action is received: + /// + /// ```swift + /// await store.send(.buttonTapped) + /// await store.receive(.response(.success(42)) { + /// $0.count = 42 + /// } + /// ``` + /// + /// Due to the variability of concurrency in Swift, sometimes a small amount of time needs to pass + /// before effects execute and send actions, and that is why this method suspends. The default + /// time waited is very small, and typically it is enough so you should be controlling your + /// dependencies so that they do not wait for real world time to pass (see + /// for more information on how to do that). + /// + /// To change the amount of time this method waits for an action, pass an explicit `timeout` + /// argument, or set the ``timeout`` on the ``TestStore``. /// /// - Parameters: /// - expectedAction: An action expected from an effect. @@ -1423,14 +1439,33 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { await Task.megaYield() } - /// Asserts a matching action was received from an effect and asserts how the state changes. + /// Asserts an action was received from an effect that matches a predicate, and asserts how the + /// state changes. /// - /// See ``receive(_:timeout:assert:file:line:)-6b3xi`` for more information on how to use this - /// method. + /// This method is similar to ``receive(_:timeout:assert:file:line:)-1rwdd``, except it allows you + /// to assert that an action was received that matches a predicate without asserting on all the + /// data in the action: + /// + /// ```swift + /// await store.send(.buttonTapped) + /// await store.receive { + /// guard case .response(.success) = $0 else { return false } + /// return true + /// } assert: { + /// store.count = 42 + /// } + /// ``` + /// + /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a grey + /// information box will show next to the `store.receive` line in Xcode letting you know what data + /// was in the effect that you chose not to assert on. + /// + /// If you only want to check that a particular action case was received, then you might find the + /// ``receive(_:timeout:assert:file:line:)-4e4m0`` overload of this method more useful. /// /// - Parameters: - /// - matchingAction: A closure that attempts to extract a value from an action. If it returns - /// `nil`, a test failure is reported. + /// - isMatching: A closure that attempts to match an action. If it returns `false`, a test + /// failure is reported. /// - nanoseconds: The amount of time to wait for the expected action. /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to /// the store. The mutable state sent to this closure must be modified to match the state of @@ -1459,13 +1494,30 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { await Task.megaYield() } - /// Asserts an action was received matching a case path and asserts how the state changes. + /// This method is similar to ``receive(_:timeout:assert:file:line:)-1rwdd``, except it allows you + /// to assert that an action was received that matches a particular case of the action enum + /// without asserting on all the data in the action. /// - /// See ``receive(_:timeout:assert:file:line:)-5n755`` for more information of how to use this - /// method. + /// It can be useful to assert that a particular action was received without asserting on the data + /// inside the action. For example: + /// + /// ```swift + /// await store.receive(/Search.Action.searchResponse) { + /// $0.results = [ + /// "CasePaths", + /// "ComposableArchitecture", + /// "IdentifiedCollections", + /// "XCTestDynamicOverlay", + /// ] + /// } + /// ``` + /// + /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a grey + /// information box will show next to the `store.receive` line in Xcode letting you know what data + /// was in the effect that you chose not to assert on. /// /// - Parameters: - /// - casePath: A case path identifying the case of an action to enum to receive + /// - casePath: A case path identifying the case of an action to enum to receive. /// - nanoseconds: The amount of time to wait for the expected action. /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to /// the store. The mutable state sent to this closure must be modified to match the state of @@ -1497,9 +1549,9 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { #if swift(>=5.7) && !os(macOS) && !targetEnvironment(macCatalyst) /// Asserts an action was received matching a case path and asserts how the state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-5n755``, except it allows - /// you to assert that an action was received that matches a particular case of the action - /// enum without asserting on all the data in the action. + /// This method is similar to ``receive(_:timeout:assert:file:line:)-4he05``, except it allows + /// you to assert that an action was received that matches a particular case of the action enum + /// without asserting on all the data in the action. /// /// It can be useful to assert that a particular action was received without asserting /// on the data inside the action. For example: @@ -1515,9 +1567,9 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { /// } /// ``` /// - /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a - /// grey information box will show next to the `store.receive` line in Xcode letting you know - /// what data was in the effect that you chose not to assert on. + /// When the store's ``exhaustivity`` is set to anything other than ``Exhaustivity/off``, a grey + /// information box will show next to the `store.receive` line in Xcode letting you know what + /// data was in the effect that you chose not to assert on. /// /// - Parameters: /// - actionCase: A case path identifying the case of an action to enum to receive @@ -1725,8 +1777,8 @@ extension TestStore { /// Useful for testing view store-specific state. /// /// - Parameter toScopedState: A function that transforms the reducer's state into scoped state. - /// This state will be asserted against as it is mutated by the reducer. Useful for testing - /// view store state transformations. + /// This state will be asserted against as it is mutated by the reducer. Useful for testing view + /// store state transformations. public func scope( state toScopedState: @escaping (ScopedState) -> S ) -> TestStore { @@ -1735,8 +1787,8 @@ extension TestStore { /// Clears the queue of received actions from effects. /// - /// Can be handy if you are writing an exhaustive test for a particular part of your feature, - /// but you don't want to explicitly deal with all of the received actions: + /// Can be handy if you are writing an exhaustive test for a particular part of your feature, but + /// you don't want to explicitly deal with all of the received actions: /// /// ```swift /// let store = TestStore(/* ... */) @@ -1818,8 +1870,8 @@ extension TestStore { /// Cancels any currently in-flight effects. /// - /// Can be handy if you are writing an exhaustive test for a particular part of your feature, - /// but you don't want to explicitly deal with all effects: + /// Can be handy if you are writing an exhaustive test for a particular part of your feature, but + /// you don't want to explicitly deal with all effects: /// /// ```swift /// let store = TestStore(/* ... */) @@ -1955,8 +2007,8 @@ extension TestStore { /// await store.send(.stopTimerButtonTapped).finish() /// ``` /// -/// See ``TestStore/finish(timeout:file:line:)`` for the ability to await all in-flight effects in -/// the test store. +/// See ``TestStore/finish(timeout:file:line:)-53gi5`` for the ability to await all in-flight +/// effects in the test store. /// /// See ``ViewStoreTask`` for the analog provided to ``ViewStore``. public struct TestStoreTask: Hashable, Sendable {