0

I'm running into an issue with snapshot testing a view that listens for updates from a view model using a Combine publisher. The issue seems to be with the sink that is received on the main queue - the closure is what updates the view state. When I attach the view model to the view, the closure inside the sink is not executed until after the snapshot test runs, and I'm left with an empty button! Is there a way to force the sink's closure to execute before running the test?

import Combine
import SnapshotTesting
import UIKit
import XCTest

struct ButtonState {
    let text: String
    let color: UIColor
}

protocol ButtonViewModel {
    var state: AnyPublisher<ButtonState, Never> { get }
}

final class MyButton: UIButton {
    private var cancellable: Cancellable?

    func attach(viewModel: some ButtonViewModel) {
        cancellable = viewModel.state
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.convertState($0)
            }
    }

    private func convertState(_ state: ButtonState) {
        setTitle(state.text, for: .normal)
        setTitleColor(state.color, for: .normal)
    }
}

struct MockViewModel: ButtonViewModel {
    var state: AnyPublisher<ButtonState, Never> {
        currentState.eraseToAnyPublisher()
    }

    private let currentState: CurrentValueSubject<ButtonState, Never>

    init(initialState: ButtonState) {
        currentState = CurrentValueSubject(initialState)
    }
}

final class ButtonSnapshotTest: XCTestCase {
    func testMyButton() {
        let myButton = MyButton()
        myButton.attach(viewModel: MockViewModel(initialState: .init(text: "Hi", color: .blue)))
        assertSnapshot(matching: myButton, as: .image)
    }
}
  • I suggest you define away the problem by saying it's the view model's responsibility to publish on the main queue, instead of doing it in `MyButton.attach`. Then your `MockViewModel` can just avoid using `receive(on:)` and so publish its first output synchronously. But if you insist, then either pass in the scheduler instead of hardcoding `DispatchQueue.main`, or use an `XCTestExpectation`. The answer to the dup question describes both of those solutions. – rob mayoff Feb 05 '23 at 18:59

0 Answers0