Тестирование презентера с использованием PromiseKit

от автора

Паттерн 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)         }     } }

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


ссылка на оригинал статьи https://habr.com/post/425069/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *