Swift модульное тестирование, просмотр модели интерфейса - PullRequest
0 голосов
/ 17 апреля 2020

Как я понимаю, лучше всего тестировать методы класса publi c.

Давайте посмотрим на этот пример. У меня есть модель представления для контроллера представления.

protocol MyViewModelProtocol {

   var items: [SomeItem] { get }
   var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }

   func viewLoaded()
}

class MyViewModel: MyViewModelProtocol {

   func viewLoaded() {
        let items = createDetailsCellModels()
        updateCellModels(with: items)
        requestDetails()
   }
}

Я хочу проверить класс viewLoaded (). Этот класс вызывает два других метода - updateItems () и requestDetails (). Один из методов устанавливает элементы, а другой вызывает API для получения данных и обновления этих элементов. Мы обновили массив элементов два раза, и onInsertItemsAtIndexPaths вызывается два раза - при настройке этих элементов и при обновлении новыми данными.

Я могу проверить, установлены ли ожидающие элементы после вызова viewLoaded () и вызван ли onInsertItemsAtIndexPaths ,

Однако метод испытаний станет довольно сложным.

Как вы считаете, стоит ли тестировать эти два метода по отдельности или просто написать этот огромный тест?

Тестируя только viewLoaded (), я считаю, что реализация может измениться, и я только все равно, что я ожидаю результатов.

1 Ответ

0 голосов
/ 23 апреля 2020

Я думаю, то же самое, должны быть протестированы только публичные c функции, так как публичные c используют частные, и ваш взгляд на MVVM верен. Вы можете улучшить его, добавив DataSource и Mapper, которые позволяют улучшить тестирование.

Однако, да, тест мне кажется огромным, тесты должны тестировать простые модули и обеспечивать работу небольших частей кода. ну, с примером, который вы показываете, сложно, вам нужно разделить на слои (чистый код). В этом примере вы загружаете данные в viewModel и затрудняете их макет. Но если у вас есть слой Domain, вы можете передать макет UseCase в viewModel и контролировать результат. Если вы запустите тест на своем примере, результат также будет зависеть от того, что возвращает конечная точка. (404, 200, пустой массив, данные с ошибкой ...). Поэтому для целей тестирования важно иметь хорошее разделение по слоям. (Презентация, домен и данные), чтобы иметь возможность тестировать каждый из них по отдельности.

Я приведу вам пример того, как я бы протестировал режим просмотра, конечно, есть лучшие и более интересные примеры, но это подход.

Здесь вы можете увидеть viewModel

protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}

protocol BeersListViewModelInput {
    func viewDidLoad()
    func updateView()
    func image(url: String?, index: Int) -> Cancellable?
}

protocol BeersListViewModelOutput {
    var items: Box<BeersListModel?> { get }
    var loadingStatus: Box<LoadingStatus?> { get }
    var error: Box<Error?> { get }
}

final class DefaultBeersListViewModel {
    private let beersListUseCase: BeersListUseCase
    private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
    var items: Box<BeersListModel?> = Box(nil)
    var loadingStatus: Box<LoadingStatus?> = Box(.stop)
    var error: Box<Error?> = Box(nil)

    @discardableResult
    init(beersListUseCase: BeersListUseCase) {
        self.beersListUseCase = beersListUseCase
    }

    func viewDidLoad() {
        updateView()
    }
}

// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
    func updateView() {
        self.loadingStatus.value = .start
        beersLoadTask = beersListUseCase.execute(completion: { (result) in
            switch result {
            case .success(let beers):
                let beers = beers.map { DefaultBeerModel(beer: $0) }
                self.items.value = DefaultBeersListModel(beers: beers)
            case .failure(let error):
                self.error.value = error
            }
            self.loadingStatus.value = .stop
        })
    }
}

// MARK: - Images
extension DefaultBeersListViewModel {
    func image(url: String?, index: Int) -> Cancellable? {
        guard let url = url else { return nil }
        return beersListUseCase.image(with: url, completion: { (result) in
            switch result {
            case .success(let imageData):
                self.items.value?.items?[index].image.value = imageData
            case .failure(let error ):
              print("image error: \(error)")
            }
        })
    }
}

Здесь вы можете увидеть тест viewModel с использованием насмешек для данных и представления.

class BeerListViewModelTest: XCTestCase {
    private enum ErrorMock: Error {
        case error
    }

    class BeersListUseCaseMock: BeersListUseCase {

        var error: Error?
        var expt: XCTestExpectation?

        func execute(completion: @escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
            let beersMock = BeersMock.makeBeerListEntityMock()
            if let error = error {
                completion(.failure(error))
            } else {
                completion(.success(beersMock))
            }
            expt?.fulfill()
            return nil
        }

        func image(with imageUrl: String, completion: @escaping (Result<Data, Error>) -> Void) -> Cancellable? {
            return nil
        }
    }

    func testWhenAPIReturnAllData() {
        let beersListUseCaseMock = BeersListUseCaseMock()
        beersListUseCaseMock.expt = self.expectation(description: "All OK")
        beersListUseCaseMock.error = nil

        let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)


        viewModel.items.bind { (_) in}
        viewModel.updateView()

        waitForExpectations(timeout: 10, handler: nil)
        XCTAssertNotNil(viewModel.items.value)
        XCTAssertNil(viewModel.error.value)
        XCTAssert(viewModel.loadingStatus.value == .stop)
    }

    func testWhenDataReturnsError() {
        let beersListUseCaseMock = BeersListUseCaseMock()
        beersListUseCaseMock.expt = self.expectation(description: "Error")
        beersListUseCaseMock.error = ErrorMock.error

        let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)

        viewModel.updateView()

        waitForExpectations(timeout: 10, handler: nil)
        XCTAssertNil(viewModel.items.value)
        XCTAssertNotNil(viewModel.error.value)
        XCTAssert(viewModel.loadingStatus.value == .stop)
    }
}

таким образом вы можете протестировать Представление, бизнес-логика c и данные отдельно, в дополнение к тому, что это код, который можно многократно использовать.

Надеюсь, это поможет вам, я разместил его на github, если вам это нужно. https://github.com/cardona/MVVM

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...