Testing your feature

Learn how to write test for the counter built in previous tutorials, including how to assert against state changes and how effects execute and feed data back into the system.

01-03-TestingYourFeature.tutorial

Testing state changes

The only thing that needs to be tested for features built in the Composable Architecture is the reducer, and that comes down to testing two things: how state mutates when actions are sent, and how effects are executed and feed their data back into the reducer.

State changes are by far the easiest part to test in the Composable Architecture since reducers form a pure function. All you need to do is feed a piece of state and an action to the reducer and then assert on how the state changed.

But, the Composable Architecture makes an easy process even easier thanks to the TestStore. The test store is a testable runtime for your feature that monitors everything happening inside the system as you send actions, making it possible for you to write simple assertions, and when your assertion fails it provides a nicely formatted failure message.

Now the test passes and so proves that the incrementing and decrementing logic does work as we expect. However, the increment and decrement logic is some of the simplest in our feature. In more real world features the logic will be a lot more complex and you will have to do more work to assert on how state changes. But luckily doing so can be ergonomic and test failure messages are user-friendly thanks to the test store.

  1. Let’s write a test for the very simple incrementing and decrementing behavior in our counter feature. We will start by creating a CounterFeatureTests.swift file with some basic scaffolding in place for the test.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
    
      }
    }
    01-03-01-code-0001.swift
  2. Next, we will create a TestStore, which is a tool that makes it easy to assert on how the behavior of your feature changes as actions are sent into the system. You create a test store in the same way you create a Store, by providing some initial state to start the feature in and providing a trailing closure to describe the reducer that will be powering the feature.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
      }
    }
    01-03-01-code-0002.swift
  3. TestStore requires equatable state in order to make its assertions, so we must add a conformance.

    CounterFeature.swift
    import ComposableArchitecture
    
    @Reducer
    struct CounterFeature {
      @ObservableState
      struct State: Equatable {
        var count = 0
        var fact: String?
        var isLoading = false
        var isTimerRunning = false
      }
    
      enum Action {
        case decrementButtonTapped
        case factButtonTapped
        case factResponse(String)
        case incrementButtonTapped
        case timerTick
        case toggleTimerButtonTapped
      }
    
      enum CancelID { case timer }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .decrementButtonTapped:
            state.count -= 1
            state.fact = nil
            return .none
    
          case .factButtonTapped:
            state.fact = nil
            state.isLoading = true
            return .run { [count = state.count] send in
              let (data, _) = try await URLSession.shared
                .data(from: URL(string: "http://numbersapi.com/\(count)")!)
              let fact = String(decoding: data, as: UTF8.self)
              await send(.factResponse(fact))
            }
    
          case let .factResponse(fact):
            state.fact = fact
            state.isLoading = false
            return .none
    
          case .incrementButtonTapped:
            state.count += 1
            state.fact = nil
            return .none
    
          case .timerTick:
            state.count += 1
            state.fact = nil
            return .none
    
          case .toggleTimerButtonTapped:
            state.isTimerRunning.toggle()
            if state.isTimerRunning {
              return .run { send in
                while true {
                  try await Task.sleep(for: .seconds(1))
                  await send(.timerTick)
                }
              }
              .cancellable(id: CancelID.timer)
            } else {
              return .cancel(id: CancelID.timer)
            }
          }
        }
      }
    }
    01-03-01-code-0003.swift
  4. Then we can start sending actions into the store in order to emulate something the user is doing. For example, we can emulate tapping the increment button and then the decrement button.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.incrementButtonTapped)
        await store.send(.decrementButtonTapped)
      }
    }
    01-03-01-code-0004.swift
  5. Run the test by typing cmd+U or clicking the test diamond next to the test method. Unfortunately, you will find that the test fails. This is because each time you send an action to a TestStore you must also describe exactly how the state changes after that action is sent. The library also helpfully shows you a detailed failure message showing you exactly how state differed from what you expected (the lines with the minus “-”) and the actual value (the lines with the plus “+”).

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.incrementButtonTapped)
        // ❌ State was not expected to change, but a change occurred: …
        //
        //       CounterFeature.State(
        //     −   count: 0,
        //     +   count: 1,
        //         fact: nil,
        //         isLoading: false,
        //         isTimerRunning: false
        //       )
        //
        // (Expected: −, Actual: +)
        await store.send(.decrementButtonTapped)
        // ❌ State was not expected to change, but a change occurred: …
        //
        //       CounterFeature.State(
        //     −   count: 1,
        //     +   count: 0,
        //         fact: nil,
        //         isLoading: false,
        //         isTimerRunning: false
        //       )
        //
        // (Expected: −, Actual: +)
      }
    }
    01-03-01-code-0005.swift
  6. To fix the test failures we need to assert how the state changed after sending each action, and the test store makes this very ergonomic. You only need to provide a trailing closure to the send method, that closure is handed a mutable version of the state before the action was sent, and it’s your job to mutate $0 so that it equals the state after the action is sent.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.incrementButtonTapped) {
          $0.count = 1
        }
        await store.send(.decrementButtonTapped) {
          $0.count = 0
        }
      }
    }
    01-03-01-code-0006.swift

Testing effects

We just tested one of the most important responsibilities of the reducer, and that is how it mutates state when it processes an action. The next important responsibility of a reducer is the effects it returns that are then processed by the store.

Writing tests against side effects takes a lot more work since you typically have to control your dependency on external systems and then provide test-friendly versions of those dependencies for tests. Let’s start by testing the timer functionality of our feature, which turns out to be a little easier to test than the network request for fetching a number fact.

This is a failure because the TestStore forces you to assert on how your entire feature evolves over time, including effects. In this case, the test store is forcing that all effects that were started in the test finish before the test is over. This can help you catch bugs, such as if you didn’t know an effect was running and it emitted actions back into the system that you did not expect, or if your state mutations from those actions had bugs. So, this is a very good failure to have, and is one of the many ways the Composable Architecture can help us catch problems in our code.

While this test does pass, it also isn’t asserting on any of the timer behavior. We would like to assert that after some time a timerTick action is sent into the system and causes the count to increment. This can be done by using the ComposableArchitecture/TestStore/receive(_:timeout:assert:fileID:file:line:column:)-53wic method on test store to assert that you expect to receive an action, and describe how state mutates upon receiving that action.

This is happening because the timer takes a full second to emit, but the test store will only wait around for a certain amount of time to receive an action, and if it doesn’t, it creates a test failure. This is because the test store doesn’t want your tests to be slow, and so it would rather you take control over your dependency on time to write a faster, more deterministic test.

However, the test now takes over 1 second to run. And say we wanted to assert on a few more timer ticks, then we would have to wait even more time. Or say we wanted to change our timer to tick only every 10 seconds. Would we really want to hold up our test suite for 10 seconds while we wait for the tick?

The fix is to not reach out to the global, uncontrollable Task.sleep function, which forces our test suite to wait around for real time to pass in order to get ticks from the timer. Instead we need to make our feature use a Swift Clock, which allows us to provide a ContinuousClock when run in simulators and devices, but in tests we can use a controllable clock, such as a test clock.

Luckily the Composable Architecture comes with a dependency management system (see Dependencies for more information), and even comes with a controllable clock out of the box.

With that little bit of upfront work to control the dependency on time-based asynchrony we can now write a very simple test that passes deterministically and immediately.

Now this test passes immediately, and we can be confident it will pass deterministically, 100% of the time. By taking control of our dependency we do not have to worry about slowing down our test suite or being afraid that we didn’t provide a big enough timeout in receive.

But, while this is all looking really good so far, our feature still has another bit of behavior that we do not have any test coverage on, and it involves another side effect. That is the behavior that loads a fact for a number, and it uses a network request to load that data.

  1. Let’s get the scaffolding of a new test into place by creating a new async test method and constructing a TestStore.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func basics() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
      }
    }
    01-03-02-code-0001.swift
  2. We want to test the flow of the user starting the timer, waiting a few seconds to see the count go up, and then the user stopping the timer. This can be done by emulating the user starting the timer by sending the toggleTimerButtonTapped, and we can even assert on how state changes since we know the isTimerRunning state should flip to true.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
      }
    }
    01-03-02-code-0002.swift
  3. However, if we run this test we get a failure. It tells us that the test ended but that an effect was still running.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        // ❌ An effect returned for this action is still running.
        //    It must complete before the end of the test. …
      }
    }
    01-03-02-code-0003.swift
  4. To get the test to pass we simply have to emulate the user toggling the timer again by sending the toggleTimerButtonTapped action.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0004.swift
  5. Add a new assertion that shows you expect to receive a timerTick action and that the count state increases to 1.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await store.receive(\.timerTick) {
          $0.count = 1
        }
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0005.swift
  6. If you run the test now you may find that sometimes it passes, albeit taking over a second to run, or sometimes it fails.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await store.receive(\.timerTick) {
          $0.count = 1
        }
        // ✅ Test Suite 'Selected tests' passed.
        //        Executed 1 test, with 0 failures (0 unexpected) in 1.044 (1.046) seconds
        //    or:
        // ❌ Expected to receive an action, but received none after 0.1 seconds.
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0006.swift
  7. One thing we can do to force the TestStore to wait for more time to receive the action is to use the timeout parameter of receive. We need to make it wait more than 1 second because Task.sleep is not an exact tool, but with the explicit timeout the test now passes.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await store.receive(\.timerTick, timeout: .seconds(2)) {
          $0.count = 1
        }
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0007.swift
  8. Go back to CounterFeature.swift and add a dependency on a continuous clock to the reducer. And then, in the implementation of reduce, do not reach out to Task.sleep and instead use the clock the feature depends on.

    CounterFeature.swift
    import ComposableArchitecture
    
    @Reducer
    struct CounterFeature {
      @ObservableState
      struct State: Equatable {
        var count = 0
        var fact: String?
        var isLoading = false
        var isTimerRunning = false
      }
      
      enum Action {
        case decrementButtonTapped
        case factButtonTapped
        case factResponse(String)
        case incrementButtonTapped
        case timerTick
        case toggleTimerButtonTapped
      }
      
      enum CancelID { case timer }
      
      @Dependency(\.continuousClock) var clock
      
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .decrementButtonTapped:
            state.count -= 1
            state.fact = nil
            return .none
            
          case .factButtonTapped:
            state.fact = nil
            state.isLoading = true
            return .run { [count = state.count] send in
              let (data, _) = try await URLSession.shared
                .data(from: URL(string: "http://numbersapi.com/\(count)")!)
              let fact = String(decoding: data, as: UTF8.self)
              await send(.factResponse(fact))
            }
            
          case let .factResponse(fact):
            state.fact = fact
            state.isLoading = false
            return .none
            
          case .incrementButtonTapped:
            state.count += 1
            state.fact = nil
            return .none
            
          case .timerTick:
            state.count += 1
            state.fact = nil
            return .none
            
          case .toggleTimerButtonTapped:
            state.isTimerRunning.toggle()
            if state.isTimerRunning {
              return .run { send in
                for await _ in self.clock.timer(interval: .seconds(1)) {
                  await send(.timerTick)
                }
              }
              .cancellable(id: CancelID.timer)
            } else {
              return .cancel(id: CancelID.timer)
            }
          }
        }
      }
    }
    01-03-02-code-0008.swift
  9. Go back to CounterFeatureTests.swift so that we can make some changes where we explicitly provide a controllable clock to use for tests.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await store.receive(\.timerTick, timeout: .seconds(2)) {
          $0.count = 1
        }
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0009.swift
  10. Construct a TestClock at the top of your testTimer method. This will be the clock we want to use in the feature’s reducer so that we can control time. To do that we provide another trailing closure to TestStore called withDependencies, and it allows you to override any dependency you want. And then finally, before receiving the timerTick action we will tell the test clock to advance by 1 second.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func timer() async {
        let clock = TestClock()
    
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        } withDependencies: {
          $0.continuousClock = clock
        }
        
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = true
        }
        await clock.advance(by: .seconds(1))
        await store.receive(\.timerTick) {
          $0.count = 1
        }
        await store.send(.toggleTimerButtonTapped) {
          $0.isTimerRunning = false
        }
      }
    }
    01-03-02-code-0010.swift

Testing network requests

Network requests are probably the most common kind of side effect in an application since most often some external server holds your users’ data. Testing features that make network requests can be difficult because making requests can be slow, can depend on your network connectivity or the server’s, and there’s no way to predict what kind of data will be sent back from the server.

Let’s try writing a test for the number fact behavior in a naive way, and see what goes wrong.

For the test we want to emulate the flow of the user tapping the fact button, seeing the progress indicator, and then some time later the fact is fed back into the system.

What we are seeing here is that there is no way to test this behavior. The server will send back a different fact each time. And even if we could predict the data sent back from the server, it still would not be ideal because our tests will become slow and flakey since they require internet connectivity and uptime of an external server.

  1. Add a new test method to CounterFeatureTests.swift for testing the number fact behavior. Also go ahead and get some scaffolding into place for the TestStore.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
      }
    }
    01-03-03-code-0001.swift
  2. Emulate the user tapping the button by sending the factButtonTapped action, and we can already assert that isLoading must flip to true.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
      }
    }
    01-03-03-code-0002.swift
  3. Unfortunately, if we run tests we see it fails. This shouldn’t be too surprising based on what we have learned above about testing. The TestStore forces us to assert on how all effects execute, and since the network request has not yet finished we are getting a failure.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        // ❌ An effect returned for this action is still running.
        //    It must complete before the end of the test. …
      }
    }
    01-03-03-code-0003.swift
  4. To fix the test we need to wait for the network request to finish and receive the factResponse action. But then the question is how do we assert on the fact returned from the server? Each time we ask for a fact from the server it may send us something different.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        await store.receive(\.factResponse, timeout: .seconds(1)) {
          $0.isLoading = false
          $0.fact = "???"
        }
      }
    }
    01-03-03-code-0004.swift
  5. Run tests to see that the test fails because we receive a fact from the server we could not have possibly predicted.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        await store.receive(\.factResponse, timeout: .seconds(1)) {
          $0.isLoading = false
          $0.fact = "???"
        }
        // ❌ A state change does not match expectation: …
        //
        //       CounterFeature.State(
        //         count: 0,
        //     −   fact: "???",
        //     +   fact: "0 is the atomic number of the theoretical element tetraneutron.",
        //         isLoading: false,
        //         isTimerRunning: false
        //       )
      }
    }
    01-03-03-code-0005.swift

Controlling dependencies

Now we see the problem with using uncontrolled dependencies in our feature code. It makes our code hard to test, and can make tests take a long time to run or become flakey.

For these reasons, and more, it is highly encouraged for you to control your dependency on external systems (see Dependencies for more information). The Composable Architecture comes with a complete set of tools for controlling and propagating dependencies throughout an application.

That is all it takes to put a controllable interface in front your dependency. With that little bit of upfront work you can start using the dependency in your features, and most importantly, start using test-friendly versions of the dependency in tests.

With that little bit of work done in the feature you can now instantly and easily write a unit test for this behavior that completely avoids the network, and will pass immediately and deterministically, 100% of the time. But, before doing that let’s show off a super power of the Composable Architecture.

The test fails with the same messages, but there is a new one. It tells us that we are using a live dependency in our test without overriding it. This is a fantastic failure to get because it notifies us whenever you may be accidentally making a network request, or writing something to disk, or tracking analytics, in a test when you don’t mean to.

We definitely do not want to make a live network request in our test, so let’s fix it, and this will make our test pass immediately and deterministically, 100% of the time. We are going to make 1 small change that instantly makes the feature testable.

That is all it takes to prepare your feature for testing. It does take a few upfront steps, but once done you can immediately use the dependency in any feature. There are even more benefits to controlling dependencies beyond writing tests, such as running Xcode previews in a controlled environment, and providing onboarding for your users that run your features in a sandbox so that you do not accidentally make changes to the outside world that you did not expect.

  1. Start by creating a new file, NumberFactClient.swift, and import the Composable Architecture. This will give you access to the tools necessary to control any dependency in your feature.

    NumberFactClient.swift
    import ComposableArchitecture
    01-03-04-code-0001.swift
  2. The first step to controlling your dependency is to model an interface that abstracts the dependency, in this case a single async, throwing endpoint that takes an integer and returns a string. This will allow you to use a “live” version of the dependency when running your feature in simulators and devices, but you can use a more controlled version during tests.

    NumberFactClient.swift
    import ComposableArchitecture
    
    struct NumberFactClient {
      var fetch: (Int) async throws -> String
    }
    01-03-04-code-0002.swift
  3. Next you need to register your dependency with the library, which requires two steps. First you conform the client to the DependencyKey protocol, which requires you to provide a liveValue. This is the value used when your feature is run in simulators and devices, and it’s the place where it is appropriate to make live network requests.

    NumberFactClient.swift
    import ComposableArchitecture
    import Foundation
    
    struct NumberFactClient {
      var fetch: (Int) async throws -> String
    }
    
    extension NumberFactClient: DependencyKey {
      static let liveValue = Self(
        fetch: { number in
          let (data, _) = try await URLSession.shared
            .data(from: URL(string: "http://numbersapi.com/\(number)")!)
          return String(decoding: data, as: UTF8.self)
        }
      )
    }
    01-03-04-code-0003.swift
  4. The second step to registering the dependency with the library is to add a computed property to DependencyValues with a getter and a setter. This is what allows for the syntax @Dependency(\.numberFact) in the reducer.

    NumberFactClient.swift
    import ComposableArchitecture
    import Foundation
    
    struct NumberFactClient {
      var fetch: (Int) async throws -> String
    }
    
    extension NumberFactClient: DependencyKey {
      static let liveValue = Self(
        fetch: { number in
          let (data, _) = try await URLSession.shared
            .data(from: URL(string: "http://numbersapi.com/\(number)")!)
          return String(decoding: data, as: UTF8.self)
        }
      )
    }
    
    extension DependencyValues {
      var numberFact: NumberFactClient {
        get { self[NumberFactClient.self] }
        set { self[NumberFactClient.self] = newValue }
      }
    }
    01-03-04-code-0004.swift
  5. Go back to CounterFeature.swift and add a new dependency using the @Dependency property wrapper, but this time for the number fact client. Then, in the effect returned from factButtonTapped, use the numberFact dependency to load the fact rather than reaching out to URLSession to make a live network request.

    CounterFeature.swift
    import ComposableArchitecture
    
    @Reducer
    struct CounterFeature {
      @ObservableState
      struct State: Equatable {
        var count = 0
        var fact: String?
        var isLoading = false
        var isTimerRunning = false
      }
      
      enum Action {
        case decrementButtonTapped
        case factButtonTapped
        case factResponse(String)
        case incrementButtonTapped
        case timerTick
        case toggleTimerButtonTapped
      }
      
      enum CancelID { case timer }
      
      @Dependency(\.continuousClock) var clock
      @Dependency(\.numberFact) var numberFact
      
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .decrementButtonTapped:
            state.count -= 1
            state.fact = nil
            return .none
            
          case .factButtonTapped:
            state.fact = nil
            state.isLoading = true
            return .run { [count = state.count] send in
              try await send(.factResponse(self.numberFact.fetch(count)))
            }
            
          case let .factResponse(fact):
            state.fact = fact
            state.isLoading = false
            return .none
            
          case .incrementButtonTapped:
            state.count += 1
            state.fact = nil
            return .none
            
          case .timerTick:
            state.count += 1
            state.fact = nil
            return .none
            
          case .toggleTimerButtonTapped:
            state.isTimerRunning.toggle()
            if state.isTimerRunning {
              return .run { send in
                for await _ in self.clock.timer(interval: .seconds(1)) {
                  await send(.timerTick)
                }
              }
              .cancellable(id: CancelID.timer)
            } else {
              return .cancel(id: CancelID.timer)
            }
          }
        }
      }
    }
    01-03-04-code-0005.swift
  6. Without making any changes to the test, run the test in Xcode again.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        await store.receive(\.factResponse, timeout: .seconds(1)) {
          $0.isLoading = false
          $0.fact = "???"
        }
        // ❌ @Dependency(\.numberFact) has no test implementation, but was
        //    accessed from a test context:
        //
        //   Location:
        //     CounterFeature.swift:70
        //   Dependency:
        //     NumberFactClient
        //
        // Dependencies registered with the library are not allowed to use
        // their default, live implementations when run from tests.
      }
    }
    01-03-04-code-0006.swift
  7. Override dependencies on the TestStore by opening the withDependencies trailing closure. This closure is passed an argument that represents the current dependencies, and you can mutate it to change the dependencies however you want. In particular, we will override the numberFact.fetch endpoint to immediately return a hard coded string. Notice that there is no true async or request work being performed in the endpoint.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        } withDependencies: {
          $0.numberFact.fetch = { "\($0) is a good number." }
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        await store.receive(\.factResponse, timeout: .seconds(1)) {
          $0.isLoading = false
          $0.fact = "???"
        }
      }
    }
    01-03-04-code-0007.swift
  8. Now that we have overridden the numberFact client to always return something predictable, we can drop the timeout from receive and properly assert on how state changes.

    CounterFeatureTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import CounterApp
    
    @MainActor
    struct CounterFeatureTests {
      @Test
      func numberFact() async {
        let store = TestStore(initialState: CounterFeature.State()) {
          CounterFeature()
        } withDependencies: {
          $0.numberFact.fetch = { "\($0) is a good number." }
        }
        
        await store.send(.factButtonTapped) {
          $0.isLoading = true
        }
        await store.receive(\.factResponse) {
          $0.isLoading = false
          $0.fact = "0 is a good number."
        }
      }
    }
    01-03-04-code-0008.swift