Паттерн 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<Void>
}
extension MelodyListPresenter {
func fetchMelodies() -> Promise<Void> {
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<T>(result: ServiceRequestResult, model: T) -> Promise<T>
, который возвращает Promise. В этом Promise, зашиты либо мелодии, либо ошибка загрузки — то что передается в качестве симулируемого результата.
protocol ServiceRequestMock {
func mock<T>(result: ServiceRequestResult, model: T) -> Promise<T>
}
extension ServiceRequestMock {
func mock<T>(result: ServiceRequestResult, model: T) -> Promise<T> {
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)
}
}
}
В итоге, у нас получился удобный инструмент для написания тестов.
Автор: крепыш