Написать модульные тесты для ObservableObject ViewModels с опубликованными результатами - PullRequest
0 голосов
/ 29 февраля 2020

Сегодня я снова столкнулся с одной проблемой объединения, и надеюсь, что кто-то из вас сможет помочь Как можно написать обычные модульные тесты для классов ObservableObjects, которые содержат атрибуты @Published? Как я могу подписаться в своем тесте на них, чтобы получить объект результата, который я могу утверждать?

Инжектированный макет для веб-службы работает правильно, функция loadProducts() устанавливает точно такие же элементы из макета в fetchedProducts массив.

Но в настоящее время я не знаю, как получить доступ к этому массиву в моем тесте после того, как он заполнен функцией, потому что кажется, что я не могу работать с ожиданиями, loadProducts() не имеет блока завершения.

Код выглядит так:

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)")
            }
        }
    }
}

На данный момент тест, который я пытаюсь написать, выглядит следующим образом:

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
    }
}

Мок выглядит так:

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 Ответ

1 голос
/ 04 марта 2020

Может быть, эта статья поможет вам

Тестирование ваших издателей комбайнов

Для решения вашей проблемы я буду использовать код из моей статьи

    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)
    }

ваш тест должен использовать эту функцию

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)
    }
...