Testing merged streams

I'm using SwiftUI and TCA to build a new component for my app. This new component has to plug into the rest of my infrastructure, receiving updates from it, and updating it in the reducer. Everything is working well, but I'm having trouble writing reliable tests.

My dependencies can be wrapped into AsyncStreams so I can use the latest concurrency APIs. Here's how I'm plugging into the system .onAppear:

switch action {
case .onAppear:
    return .merge (
        .run { send in
            for await _ in previewStateBridge.subscribe() {
                await send(.backingStateChanged)
            }
        },
        .run { send in
            for await _ in controlBar.availableSmartMasksDescriptionStream {
                await send(.availableMasksChanged)
            }
        },
        .run { send in
            for await _ in controlBar.hasAccessToMasksStream {
                await send(.maskAccessUpdated)
            }
        },
        .run { send in
            for await images in maskPreviewProvider.subscribe() {
                await send(.maskPreviewsUpdated(images))
            }
        }
    )
   .cancellable(id: Cancellable.previewStore)
case .onDisappear:
    return .cancel(id: Cancellable.previewStore)
}

However, in my tests, it appears that these initialization actions are running in an unpredictable order, which makes it hard to write stable tests.

In my tests, I've mocked all these dependencies. As an example, one of them looks like this:

class MockMaskPreviewGenerator: MaskPreviewProvider {
    private var continuations: [AsyncStream<[Mask.ID: UIImage?]>.Continuation] = []

    func notifySubscribers(_ images: [Mask.ID: UIImage?]) {
        for continuation in continuations {
            continuation.yield(images)
        }
    }

    func subscribe() -> AsyncStream<[Mask.ID: UIImage?]> {
        AsyncStream { continuation in
            continuations.append(continuation)
            continuation.yield([:])
        }
    }
}

I would appreciate any insight into this.

My best attempt so far has been to break apart the different effects each into its own unit test and to skip receiving the other actions using await store.skipReceivedActions() but that's also proving unreliable because the order of these asynchronous streams also affects the expected state.

Only other idea I have is to wrap these 4 dependencies into a single dependency manager which provides a single action callback with an associated value of which dependency changed. Just curious if that's how others have solved the problem?

1 Like

I just wanted to follow-up and say that part of my issue is that I was not fully mocking out all my dependencies. Once I did that, I was able to control exactly when and if they yield any continuations back to the preview store, which allowed me to write predictable tests.

Glad you figured it out @jtaby! I had started to reply to suggest exactly that as it is definitely the way to get some determinism in such tests.

Thanks @mbrandonw, and apologies if I wasted any time.