Unit Test for getting the PassthroughSubject publisher's value

Viewed 1154

Is there a way to get the value of a PassthroughSubject publisher in a Unit test? I want to test that a function returns success and to test this one I want to see when the publisher value is .loaded, then is success.

class HomeViewModel: ObservableObject {

    var homeState = PassthroughSubject<StatePublisher, Never>()

    func load(item: HomeModel) {
        self.homeState.send(.loading)
        self.dataSource.load(item: item) { result in
            switch result {
            case .success:
                self.homeState.send(.loaded)
            case let .failure(error):
                self.homeState.send(.error(message: error.localizedDescription))
            }
        }
    }

}
class HomeViewModelTests: XCTestCase {
    var sut: ViewModel!
    var subscriptions = Set<AnyCancellable>()
    
    override func setUpWithError() throws {
        sut = ViewModel()
    }

    override func tearDownWithError() throws {
        sut = nil
        subscriptions = []
    }
        
    func testUpdateHomeSuccess() {
        let expected = StatePublisher.loaded
        var result = StatePublisher.loading
    
        sut.load(item: HomeModel.fixture())
        
        sut.homeState
            .sink(receiveValue: { state in
                result = state
            })
            .store(in: &subscriptions)
    
        XCTAssert(
            result == expected,
            "Home expected to be \(expected) but was \(result)"
        )
    }
    
}

I tried a test like this, but sink is never called.

2 Answers

Presumably, dataSource.load operates asynchronously. That means you need to use an XCTestExpectation in your test case. Also, because you're using a PassthroughSubject rather than a CurrentValueSubject, you should create your subscription before you start the load, to be sure you can't miss any published values.

let ex = XCTestExpectation()
sut.homeState
    .sink {
        if case .loaded = $0 {
            ex.fulfill()
        }
    }
    .store(in: &subscriptions)

wait(for: [ex], timeout: 10)
// Test fails if it doesn't call `ex.fulfill()` within 10 seconds.

Read Apple's guide to Testing Asynchronous Operations with Expectations.

I changed my PassthroughSubject to CurrentValueSubject as then the state of the value is present also when executing the XCTestCase.

So this test was always failing: (the PassthroughSubject is declared in viewModel class which is tested)

var someValue: PassthroughSubject<Bool, Never> = .init()
func someTest() throws {
    let exp = expectation(description: "Value changed")
    
    viewModel.functionChangingTheValue()

    viewModel.someValue.sink { result in
        XCTAssertTrue(result)
        exp.fulfill()
    }
    .store(in: &cancellables)

    wait(for: [exp], timeout: 0.1)
}

But this one is now successful

var someValue: CurrentValueSubject<Bool, Never> = .init(false)
func someTest() throws {
    let exp = expectation(description: "Value changed")
    
    viewModel.functionChangingTheValue()

    viewModel.someValue.sink { result in
        XCTAssertTrue(result)
        exp.fulfill()
    }
    .store(in: &cancellables)

    wait(for: [exp], timeout: 0.1)
}
Related