[Из песочницы] Тестирование презентера с использованием PromiseKit

habr.png

Паттерн MVP в разработке мобильных приложений — это довольно простой способ разгрузить ViewController и вынести часть логики в презентер. Презентер начинает обрастать логикой, которая легко поддается тестированию.

Пусть есть экран MelodyListViewController показывающий список мелодий. У него есть презентер MelodyListPresenter, который говорит ViewController что показывать. Данные презентер будет брать из сервиса MelodyService. MelodyService это обертка над базой данных и api клиентом, загружающая мелодии. Если сеть доступна, сервис берет данные с api, иначе с базы данных. Типы ошибок загрузки представлены в enum ServiceRequestError.

protocol MelodyListViewController: class {
    func showMelodies(melodies: [Melody])
    func showLoadError(error: ServiceRequestError)
}

protocol MelodyListPresenter {
    var view: MelodyListViewController? { get }
    var melodyService: MelodyService { get }

    func fetchMelodies() -> Promise
}

extension MelodyListPresenter {
    func fetchMelodies() -> Promise {
        return melodyService.getMelodies().done { melodies in
            self.view?.showMelodies(melodies: melodies)
        }.catch { error in
            self.view?.showLoadError(error: error)
        }
    }
}

protocol MelodyService {
    func getMelodies() -> Promise<[Melody]>
}

public enum ServiceRequestError: Error {
    case unknownError
    case noNetwork
    case noData
}

Построив такую структуру экрана, можно заняться тестированием. А именно тестированием получения данных презентером. Презентер имеет в зависимости MelodyService, поэтому необходимо мокировать этот протокол. Условимся, что Melody имеет статический метод mocks, который возвращает список произвольных мелодий.

class MelodyServiceMock: MelodyService, ServiceRequestMock {

    var emulatedResult: ServiceRequestResult = .error(.unknownError)

    func getMelodies() -> Promise<[Melody]> {
        let melodies = Melody.mocks()
        return mock(result: emulatedResult, model: melodies)
    }
}

enum ServiceRequestResult {
    case success
    case error(ServiceRequestError)
}

Также мокируем ViewController.

class MelodyListViewControllerMock: MelodyListViewController {

    var shownMelodies: [Melody]?
    var shownError: ServiceRequestError?

    func showMelodies(melodies: [Melody]) {
        shownMelodies = melodies
    }

    func showLoadError(error: ServiceRequestError) {
        shownError = error
    }
}

ServiceRequestMock это протокол, имеющий единственный метод func mock(result: ServiceRequestResult, model: T) -> Promise, который возвращает Promise. В этом Promise, зашиты либо мелодии, либо ошибка загрузки — то что передается в качестве симулируемого результата.

protocol ServiceRequestMock {
    func mock(result: ServiceRequestResult, model: T) -> Promise
}

extension ServiceRequestMock {
    func mock(result: ServiceRequestResult, model: T) -> Promise {
        return Promise { seal in
            switch result {
            case .success:
                return seal.fulfill(model)
            case .error(let requestError):
                return seal.reject(requestError)
            }
        }
    }
}

Таким образом мы предоставили все необходимое для тестирования презентера.

import XCTest
import PromiseKit

class MelodyListPresenterTests: XCTestCase {

    let view = MelodyListViewControllerMock()
    let melodyService = MelodyServiceMock()
    var presenter: MelodyListPresenterImp!

    override func setUp() {
        super.setUp()
        presenter = MelodyListPresenterImp(
            melodyService: melodyService, 
            view: view)
        view.presenter = presenter
    }

    func test_getMelodies_success() {
        // given
        let melodiesMock = Melody.mocks()
        melodyService.emulatedResult = .success

        // when
        let fetchMelodies = presenter.fetchMelodies()

        // then
        fetchMelodies.done { melodies in
            XCTAssertNotNil(self.view.shownMelodies)
            XCTAssert(self.view.shownMelodies == melodiesMock)
        }.catch { _ in
            XCTFail("Failed melodies upload")
        }
    }

    func test_getMelodies_fail() {
        // given
        melodyService.emulatedResult = .error(.noNetwork)

        // when
        let fetchMelodies = presenter.fetchMelodies()

        // then
        fetchMelodies.done { melodies in
            XCTFail("Mistakenly uploaded melodies")
        }.catch { _ in
            XCTAssertNotNil(self.view.shownError)
            XCTAssert(self.view.shownError is ServiceRequestError)
            XCTAssert(self.view.shownError as! ServiceRequestError == .noNetwork)
        }
    }
}

В итоге, у нас получился удобный инструмент для написания тестов.

© Habrahabr.ru