Home > Enterprise >  Success case is not being called from a viewModel test
Success case is not being called from a viewModel test

Time:10-31

I am new to working with dependency injection. When I run a test with a mocked object the success result case isn't getting triggered and the object isn't added to my @Published property which I am trying to test.

When I go to test my viewModel I can see the array to be tested but when getShows() gets called in the test the Result case is used.

Test File

import XCTest
@testable import PopularTVViewer

class PopularTVViewerTests: XCTestCase {

    func testPopularTVModel() {
        let mockedManager = MockedPopularTVManager()
        mockedManager.result = .success(mockedManager.mockPopularShows)
        let viewModel = PopularTVViewModel(manager: mockedManager)
        #warning("getShows() success case isn't being called even through viewModel has its reference.")
        viewModel.getShows()

        XCTAssertNotNil(viewModel)
        XCTAssertNotNil(mockedManager.result)
//        Currently failing
//        XCTAssertEqual(viewModel.popularTV.count, 4)
        XCTAssertEqual(mockedManager.getPopularShowsCallCounter, 1)
    }
}

MockedManager

class MockedPopularTVManager: PopularTVManagerProtocol {
    
    var result: Result<[PopularTV], NetworkError>!
    var getPopularShowsCallCounter = 0

    func getPopularShows(completion: @escaping (Result<[PopularTV], NetworkError>) -> Void) {
        completion(result)
       getPopularShowsCallCounter  = 1

    }
    
    let mockPopularShow = PopularTV(name: "House of the Dragon", posterPath: "/mYLOqiStMxDK3fYZFirgrMt8z5d.jpg", popularity: 4987.163, voteAverage: 7.7, voteCount: 881)
    
     let mockPopularShows = [
    PopularTV(name: "The Lord of the Rings: The Rings of Power", posterPath: "/mYLOqiStMxDK3fYZFirgrMt8z5d.jpg", popularity: 4987.163, voteAverage: 7.7, voteCount: 881),
    PopularTV(name: "House of the Dragon", posterPath: "/z2yahl2uefxDCl0nogcRBstwruJ.jpg", popularity: 4979.127, voteAverage: 8.6, voteCount: 1513),
    PopularTV(name: "She-Hulk: Attorney at Law", posterPath: "/hJfI6AGrmr4uSHRccfJuSsapvOb.jpg", popularity: 2823.739, voteAverage: 7.1, voteCount: 846),
    PopularTV(name: "Dahmer – Monster: The Jeffrey Dahmer Story", posterPath: "/f2PVrphK0u81ES256lw3oAZuF3x.jpg", popularity: 1774.56, voteAverage: 8.3, voteCount: 402)
]
}

ViewModel

final class PopularTVViewModel: ObservableObject {
    @Published var popularTV = [PopularTV]()
    let manager: PopularTVManagerProtocol
    let columns = ColumnLayoutHelper.threeColumnLayout
    
    // Injecting for testing.
    init(manager: PopularTVManagerProtocol = PopularTVManager()) {
        self.manager = manager
    }
    
    // Grabbing next page of results
    func getMoreShows() {
        getShows()
    }
    
    // Initial network call. 
    func getShows() {
        manager.getPopularShows() { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let popularTV):
                    for show in popularTV {
                        self?.popularTV.append(show)
                    }
                case .failure(let error):
                    switch error {
                    case .invalidURL:
                        print("Invalid URL")
                    case .invalidData:
                        print("Invalid Data")
                    case .unableToComplete:
                        print("Unable to complete")
                    case .invalidResponse:
                        print("Invalid response")
                    }
                }
            }
        }
    }
}

I've done everything I can think of to make sure the object exists and the viewModel has access to it but its as if the mockedManager in my test doesn't have the success when getShows() is run.

CodePudding user response:

The test is failing because you are switching threads during your test. The DispatchQueue.main.async in your PopularTVViewModel is causing it to fail.

You need to remove the use of DispatchQueue.main.async from your test. This is possible to do.

We first need to create a function that will stand in place of the call to DispatchQueue.main.async. This function will check that we are on the main thread and if we are execute the code directly without switching threads, otherwise if we are on a background thread it will dispatch on the main thread. This should mean that in your application it works exactly the same as before, and in your tests it avoids the thread hop so that they now pass.

/// You could make this a global function, an extension on DispatchQueue, 
/// the choice where to put it is up to you, but it should be accessible
/// by whichever classes need to use it as chances are you may need to use 
/// it in multiple places.
func performUIUpdate(using closure: @escaping () -> Void) {
    if Thread.isMainThread {
        closure()
    } else {
        DispatchQueue.main.async(execute: closure)
    }
}

We can then update your PopularTVViewModel to use the new function.

final class PopularTVViewModel: ObservableObject {
    @Published var popularTV = [PopularTV]()
    let manager: PopularTVManagerProtocol
    let columns = ColumnLayoutHelper.threeColumnLayout
    
    // Injecting for testing.
    init(manager: PopularTVManagerProtocol = PopularTVManager()) {
        self.manager = manager
    }
    
    // Grabbing next page of results
    func getMoreShows() {
        getShows()
    }
    
    // Initial network call. 
    func getShows() {
        manager.getPopularShows() { [weak self] result in
            performUIUpdate { // Note we use the new function here instead of DispatchQueue.main.async
                switch result {
                case .success(let popularTV):
                    // you could use the following instead of your for loop.
                    // self?.popularTV.append(contentsOf: popularTV)
                    for show in popularTV {
                        self?.popularTV.append(show)
                    }
                case .failure(let error):
                    switch error {
                    case .invalidURL:
                        print("Invalid URL")
                    case .invalidData:
                        print("Invalid Data")
                    case .unableToComplete:
                        print("Unable to complete")
                    case .invalidResponse:
                        print("Invalid response")
                    }
                }
            }
        }
    }
}

Your tests should now pass.

There is a great article by John Sundell that shows how to reduce flakiness in testing.

Also this book, iOS Unit Testing by Example, by Jon Reid is very good and well worth having on your bookshelf.

  • Related