Testing state changes
The library comes with a tool specifically designed to test features simply and concisely. It’s calledTestStore, and it is constructed similarly to Store by providing the initial state of the feature and the Reducer that runs the feature’s logic:
Tests that use
TestStore should be marked as async since most assertion helpers on TestStore can suspend. And while tests do not require the main actor, TestStore is main actor-isolated, and so we recommend annotating your tests and suites with @MainActor.Asserting state changes
Test stores have asend method, but it behaves differently from stores and view stores. You provide an action to send into the system, but then you must also provide a trailing closure to describe how the state of the feature changed after sending the action:
Testing multiple actions
You can send multiple actions to emulate a script of user actions and assert each step of the way how the state evolved:Testing effects
Testing state mutations is powerful, but is only half the story. The second responsibility of reducers, after mutating state from an action, is to return anEffect that encapsulates a unit of work that runs in the outside world and feeds data back into the system.
Basic effect testing
Suppose we have a feature with a button such that when you tap it, it starts a timer that counts up until you reach 5, and then stops:receive method which allows you to assert which action you expect to receive from an effect:
We are using key path syntax
\.timerTick to specify the case of the action we expect to receive. This works because the @Reducer macro automatically applies the @CasePathable macro to the Action enum.Controlling dependencies
The example above requires waiting for real time to pass, which makes tests slow. To fix this, we can add a clock dependency:Non-exhaustive testing
Exhaustive testing is powerful but can be a nuisance for highly composed features. Sometimes you may want to test in a non-exhaustive style.Assert only what you care about
You can now assert on just the high-level details:The test will pass even though we didn’t assert on all state changes in the login feature.
Testing gotchas
Testing host application
When an application target runs tests, it actually boots up a simulator and runs your actual application entry point. This can cause issues with dependency access.Long-living test stores
Test stores should always be created in individual tests, not as shared instance variables:finish:
Statically linking tests
If you statically link theComposableArchitecture module to your tests target, its implementation may clash with the implementation linked to the app itself.
Solution: Remove the static link to ComposableArchitecture from your test target. In Xcode, go to “Build Phases” and remove it from “Link Binary With Libraries”. When using SwiftPM, remove it from the testTarget’s dependencies array.
Best practices
Use hard-coded values
Assert with exact values rather than calculations in your test closures
Test effects exhaustively
Always assert on actions received from effects
Control dependencies
Use dependency injection for clocks, UUIDs, dates, and other controlled values
Non-exhaustive for integration
Use non-exhaustive testing for complex feature integration tests