Write unit tests for ObservableObject ViewModels with Published results

淺唱寂寞╮ 提交于 2021-01-05 07:23:08

问题


Today again one combine problem I currently run in and I hope that someone of you can help. How can normal unit tests be written for ObservableObjects classes which contain @Published attributes? How can I subscribe in my test to them to get the result object which I can assert?

The injected mock for the web service works correctly, loadProducts() function set exactly the same elements from the mock in the fetchedProducts array.

But I don't know currently how to access this array in my test after it is filled by the function because it seems that I cannot work with expectations here, loadProducts() has no completion block.

The code looks like this:

class ProductsListViewModel: ObservableObject {
    let getRequests: GetRequests
    let urlService: ApiUrls

    private let networkUtils: NetworkRequestUtils

    let productsWillChange = ObservableObjectPublisher()

    @Published var fetchedProducts = [ProductDTO]()
    @Published var errorCodeLoadProducts: Int?

    init(getRequestsHelper: GetRequests, urlServiceClass: ApiUrls = ApiUrls(), utilsNetwork: NetworkRequestUtils = NetworkRequestUtils()) {
        getRequests = getRequestsHelper
        urlService = urlServiceClass
        networkUtils = utilsNetwork
    }


    // nor completion block in the function used
    func loadProducts() {
        let urlForRequest = urlService.loadProductsUrl()

        getRequests.getJsonData(url: urlForRequest) { [weak self] (result: Result<[ProductDTO], Error>) in
            self?.isLoading = false
            switch result {
            case .success(let productsArray):
                // the products filled async here
                self?.fetchedProducts = productsArray
                self?.errorCodeLoadProducts = nil
            case .failure(let error):
                let errorCode = self?.networkUtils.errorCodeFrom(error: error)
                self?.errorCodeLoadProducts = errorCode
                print("error: \(error)")
            }
        }
    }
}

The test I try to write looks like this at the moment:

import XCTest
@testable import MyProject

class ProductsListViewModelTest: XCTestCase {
    var getRequestMock: GetRequests!
    let requestManagerMock = RequestManagerMockLoadProducts()

    var productListViewModel: ProductsListViewModel!

    override func setUp() {
        super.setUp()

        getRequestMock = GetRequests(networkHelper: requestManagerMock)
        productListViewModel = ProductsListViewModel(getRequestsHelper: getRequestMock)
    }

    func test_successLoadProducts() {
        let loginDto = LoginResponseDTO(token: "token-token")
        UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)

        productListViewModel.loadProducts()

        // TODO access the fetchedProducts here somehow and assert them
    }
}

The Mock looks like this:

class RequestManagerMockLoadProducts: NetworkRequestManagerProtocol {
    var isSuccess = true

    func makeNetworkRequest<T>(urlRequestObject: URLRequest, completion: @escaping (Result<T, Error>) -> Void) where T : Decodable {
        if isSuccess {
            let successResultDto = returnedProductedArray() as! T
            completion(.success(successResultDto))
        } else {
            let errorString = "Cannot create request object here"
            let error = NSError(domain: ErrorDomainDescription.networkRequestDomain.rawValue, code: ErrorDomainCode.unexpectedResponseFromAPI.rawValue, userInfo: [NSLocalizedDescriptionKey: errorString])

            completion(.failure(error))
        }
    }

    func returnedProductedArray() -> [ProductDTO] {
        let product1 = ProductDTO(idFromBackend: "product-1", name: "product-1", description: "product-description", price: 3.55, photo: nil)
        let product2 = ProductDTO(idFromBackend: "product-2", name: "product-2", description: "product-description-2", price: 5.55, photo: nil)
        let product3 = ProductDTO(idFromBackend: "product-3", name: "product-3", description: "product-description-3", price: 8.55, photo: nil)
        return [product1, product2, product3]
    }
}

回答1:


Maybe this article can help you

Testing your Combine Publishers

To solve your issue I will use code from my article

    typealias CompetionResult = (expectation: XCTestExpectation,
                                 cancellable: AnyCancellable)
    func expectValue<T: Publisher>(of publisher: T,
                                   timeout: TimeInterval = 2,
                                   file: StaticString = #file,
                                   line: UInt = #line,
                                   equals: [(T.Output) -> Bool])
        -> CompetionResult {
        let exp = expectation(description: "Correct values of " + String(describing: publisher))
        var mutableEquals = equals
        let cancellable = publisher
            .sink(receiveCompletion: { _ in },
                  receiveValue: { value in
                      if mutableEquals.first?(value) ?? false {
                          _ = mutableEquals.remove(at: 0)
                          if mutableEquals.isEmpty {
                              exp.fulfill()
                          }
                      }
            })
        return (exp, cancellable)
    }

your test needs to use this function

func test_successLoadProducts() {
        let loginDto = LoginResponseDTO(token: "token-token")
        UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)

/// The expectation here can be extended as needed

        let exp = expectValue(of: productListViewModel .$fetchedProducts.eraseToAnyPublisher(), equals: [{ $0[0].idFromBackend ==  "product-1" }])

        productListViewModel.loadProducts()

        wait(for: [exp.expectation], timeout: 1)
    }



回答2:


The easy and clearest way for me is simply to test @published var after X seconds. An example bellow :

func test_successLoadProducts() {
    let loginDto = LoginResponseDTO(token: "token-token")
    UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)

    productListViewModel.loadProducts()

    // TODO access the fetchedProducts here somehow and assert them

    let expectation = XCTestExpectation()
    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        XCTAssertEqual(self.productListViewModel.fetchedProducts, ["Awaited values"])

        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5.0)
}

I hope that helps !



来源:https://stackoverflow.com/questions/60467247/write-unit-tests-for-observableobject-viewmodels-with-published-results

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!