A typical iOS project based on the Combine framework at some point defines interfaces that are represented by ‘AnyPublisher’ or ‘AnySubscriber’. When it comes to testing Combine there are several ways of doing that which I described in my other article (How to test iOS apps with Combine, ReactiveSwift and RxSwift). However, I strongly believe that tests should be optimized for readability and ease of future maintenance. In this article, I want to go a step further and improve the testing of the Combine based abstractions.

Inputs & outputs

There are many architectural patterns i.e. MVVM, that might use a lot of Combine abstractions on its interfaces. For instance, a ViewModel might look like this:

struct SearchViewModel {
    let searchTaps: AnySubscriber<Void, Never>
    let textChanges: AnySubscriber<String, Never>
    let changeTextExternally: AnyPublisher<String, Never>
    let cancelTaps: AnySubscriber<Void, Never>
    let searchResults: AnyPublisher<SearchResult, Never>

There are both Subscribers and Publishers in this example.

However, if you want to test a bit more complicated scenario, like inspecting search results based on a combination of searchTaps and external search text changes (i.e. search query picked from search history) then you’re going to have a bad time. I think I would go with the ‘sink’ testing method, subscribe on searchResults with some subject and then somehow produce events on changeTextExternally publisher and searchTaps subscriber.

The point is, that all this setup required for testing such a heavy ‘Combine-based’ interface is way too long, especially when it comes to the particular test’s readability.

Introducing a utility that helps test Publishers and Subscribers

I suggest introducing a utility structure that takes test simplicity to the next level. The idea is to represent:

  • AnySubscriber<T, Never> as PassthroughSubject<T, Never> - this simplifies simulating new events emission, just by running i.e. searchTaps.send(...)
  • AnyPublisher<T, Never> as CurrentValueSubject<T?, Never> - value of subject, like searchResults.value represents the latest value sent by publisher. It’s optional because there might be no event emitted.

So what I usually do is to represent such ‘ViewModel’ with its “subjects” representation for tests:

extension SearchViewModel
    struct TestUtil {
        let searchTaps: PassthroughSubject<Void, Never>
        let textChanges: PassthroughSubject<String, Never>
        let changeTextExternally: CurrentValueSubject<String?, Never>
        let cancelTaps: PassthroughSubject<Void, Never>
        let searchResults: CurrentValueSubject<SearchResult?, Never>

which in the end in sequential scenarios like the one I described previously looks like this:

testUtil.changeTextExternally.send("some query")
XCTAssertEqual(searchResults.value, [...])

The connection between Combine interfaces and TestUtil is straightforward so I will skip that part for now.

But that’s a lot of boilerplate

Yeah… While the test case looks better that still requires some boilerplate code for supporting it. Here are some of the questions that possibly you would ask me right now:

  • How is this better than doing the same in a test case - it’s just adding several new properties that wrap a tested unit
  • It gets even worse when the original interface changes - then I have to update that boilerplate code as well as the test case

And I agree, it still needs improvement, and the improvement should be the automatic generation of the utility.

The best method I know to automate such code generation is to use Sourcery. It scans the codebase and propagates matching types through the generation template. I created a gist for that template so that you can easily use it for your own needs.

After all, with code generation automated I can write tests more effectively. I’m not spending any time preparing my Combine interfaces for testing nor have I have to worry about it when it comes to potential future interface changes.