0

So I'm trying to write a unit test to test a view model function however it is an Async function and in the test I'm trying to write it will run synchronously. I've seen a few examples online e.g

listViewModel.$list.sink { _ in
    XCTAssertEqual(listViewModel.list?.items.count , 1)
    actionExpectation.fulfill()
}
wait(for: [actionExpectation], timeout: 1)

and also trying the solution given here Unit testing an @ObservableObject in Swift Test Driven Development

I've not had any luck trying to test it, any ideas?

ViewModel

class ListViewModel: ObservableObject {
    @Published var list: List?
    
    let services: Services

    init(services: Services) {
        self.services = services
        getList()
    }

    func getList() {
        if let listService = self.services.resolve(ListService.self) {
            ListService().getList() { [weak self] result in
                switch result {
                    case .success(let dataRecieved):
                        DispatchQueue.main.async {
                            self?.list = dataRecieved
                        }
                    case .failure(let error):
                        print(error)
                }
            }
        }
    }
}
  • You need to store your subscriber chain in a `Set` or it will be released before it fires. Also, your `ListService` should be mocked; Unit tests should not have external dependencies such as a network service. – Paulw11 May 08 '21 at 02:47
  • @Paulw11 Sorry I'm abit new to testing with swiftui/combine. Any chance you have an example of how `Set` should be used? My ListService is already mocked and I'm always sending back a completion. I've just edited the answer – Mehul Mandalia May 08 '21 at 12:21
  • @Paulw11 the services param passes a mock Services class that contains the mock list service. It definetly calls the mock service – Mehul Mandalia May 08 '21 at 12:25
  • https://stackoverflow.com/questions/61889158/swift-combine-how-setanycancellable-works – Paulw11 May 08 '21 at 12:28
  • Just took a look still not sure how I can apply that to `@Published var list: List?` Not sure if you would be able to but could you create an example answer? I can see in the example he calls .handleEvents & .sink on repo.syncObjects(), would I have to rewrite my Service too? As he hasn't shown how his syncObject func is declared – Mehul Mandalia May 08 '21 at 13:22
  • Just add the `disposables` property to your test class and add the `.store(in:&disposables)` to the end of your subscriber chain. – Paulw11 May 08 '21 at 16:58

1 Answers1

0

Assuming your environment and service are valid for testing try this one

let listener = listViewModel.$list.sink { _ in
    XCTAssertEqual(listViewModel.list?.items.count , 1)
    actionExpectation.fulfill()
}
wait(for: [actionExpectation], timeout: 1)
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This is pretty much what I tried but the fulfil gets called before the completion of the .success block in the viewModel: case .success(let dataRecieved): DispatchQueue.main.async { self?.list = dataRecieved } I can see the service gets called as normal and returns it completion, but then instead of the viewModels getList completion it goes to the fulfill first instead – Mehul Mandalia May 08 '21 at 11:47
  • Only thing I can think of is that when I run the rest the SceneDelegate creates the View/ViewModel and then I instantiate a new viewModel in my tests. Only thing I can think of that's causing the issue. As the getLists is called as the viewModel is initialised – Mehul Mandalia May 08 '21 at 12:00
  • Have you tried skipping the first? .dropFirst() – Ryan May 08 '21 at 19:31