DEV Community

Oren Idan Yaari
Oren Idan Yaari

Posted on

Testable Apps: Why You Should Consider The Composable Architecture

Recently, I've been seeing some discussion around whether integrating TCA into our app is necessary. I wanted to take a moment to address that here.

So, why should we consider using The Composable Architecture (TCA)? Well, it's been purpose-built with testing in mind right from the start. If you're working with SwiftUI, it offers a seamless way to incorporate test coverage into your codebase. Plus, it empowers us to simulate complex user flows effortlessly. Imagine seamlessly changing a state deep within a screen and then validating that state change in its parent screen – TCA makes tasks like these a breeze.

But I get it, some might wonder if the benefits outweigh the added complexity of integrating an external library. Let's explore that with an example.

Imagine we're developing a new feature. Following Apple's recommendation to use Swift and SwiftUI, we start building our feature.
Apple quote recommendation to use Swift and SwiftUI
We create a simple ViewModel to manage some state:

class ViewModel: ObservableObject {
    struct State {
        var iLikeTest = false
    }
    @Published var state = State()

    func someFunction() {
        state.iLikeTest = true
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's say we want to test whether a specific action changes the state correctly:

class BlogCodeTests: XCTestCase {
    func testSomething() {
        let sut = ViewModel()
        sut.$state.sink { newState in
            XCTAssertFalse(newState.iLikeTest)
        }
        sut.someFunction()
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach works, but it's not without its drawbacks. For instance, when Apple introduces a new observation macro in iOS 17 for performance enhancements, our tests break because we've removed Combine and @Published. Now there is no way to observe the state outside a SwiftUI View.

@Observable
class ViewModel {
    struct State {
        var iLikeTest = false
    }
    var state = State()

    func someFunction() {
        state.iLikeTest = true
    }
}
Enter fullscreen mode Exit fullscreen mode

To fix this, we need to refactor our tests to use a global observation function. It's a cumbersome solution and can make certain flows untestable.

func testSomething() {
        let exp = expectation(description: "fulfill onChange")
        let sut = ViewModel()
        withObservationTracking {
            sut.state.iLikeTest
        } onChange: {
            XCTAssertTrue(sut.state.iLikeTest)
            exp.fulfill()
        }
        sut.someFunction()
        waitForExpectations(timeout: 1)
    }
Enter fullscreen mode Exit fullscreen mode

This doesn't really work. The test will fail because onChange will still show the value to be false. We might be able to find a way around it, but this is where TCA comes into action. Let's take a look at how we could implement the same functionality with TCA:

@Reducer
struct MainStore {
    @ObservableState
    struct State: Equatable {
        var iLikeTest = false
    }

    enum Action {
        case someAction
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .someAction:
                state.iLikeTest = true
                return .none
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And now, our test becomes much simpler and more robust:

func testReducer() async {
    let testStore = TestStore(initialState: .init()) {
        MainStore()
    }

    await testStore.send(.someAction) {
        $0.iLikeTest = true
    }
}
Enter fullscreen mode Exit fullscreen mode

No need to overthink it. Just call an action on a store, change to the expected state, and voila! If we don't change the state, we get a failure. If we forget to override a dependency, we get a failure. What's even better that it is all built on top of Apple's observable macros.
So, before making a decision, I'd encourage you to delve deeper into TCA and compare it against the vanilla SwiftUI approach.

Sentry mobile image

Improving mobile performance, from slow screens to app start time

Based on our experience working with thousands of mobile developer teams, we developed a mobile monitoring maturity curve.

Read more

Top comments (0)

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay