Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CodeFest 2019. Иван Букшев (ЦФТ) — Создание Moc...

CodeFest
April 06, 2019

CodeFest 2019. Иван Букшев (ЦФТ) — Создание MockServer’a для сурового финансового продукта

«Какая польза от автотестов?», «Какие средства для UI- тестирования есть в мире iOS-разработки?», «Кто должен писать UI-тесты?» — в докладе речь пойдет совсем не про это. Данная история будет освещать технические тонкости и подводные грабли в реализации MockServer’a — фреймворка, позволяющего подменять ответы на запросы от сервера.

Краткое содержание:
— Подходы к реализации MockServer’a.
— Настройка окружения для каждого UI-теста.
— Настройка уникальных конфигураций для UI-тестов.
— Процесс развития ответов от MockServer’а: от одиночных ответов до настраиваемой псевдодинамики.
— Решение проблем с версионированием API с последующей автоматизацией.

CodeFest

April 06, 2019
Tweet

More Decks by CodeFest

Other Decks in Technology

Transcript

  1. Mock Server для сурового финансового продукта Букшев Иван Евгеньевич Старший

    инженер-программист мобильных приложений Центр Финансовых Технологий (ЦФТ), Денежные Переводы Новосибирск, 2019 г.
  2. Рост количества первоисточников проблем !3 iOS-команда (чел.) 0 13 25

    38 50 апр'17 июл'17 окт'17 янв'18 апр'18 июл'18 окт'18 янв'19 апр'19 июл'19 окт'19 янв'20
  3. Эффективность подхода !4 1CI-агент с 2-мя симуляторами или 5 QA-инженеров

    с 2-мя девайсами 0 100 200 300 400 500 600 700 800 0 3 6 9 12 тест-кейсы инженерные часы
  4. Summary Аргументы «А, давайте!» • Минимизация ошибок в production за

    счёт более быстрого реагирования на возникающие проблемы в период разработки. • Сокращение времени на регрессионное тестирование за счёт автоматического прогона основных (70-80%) тест-кейсов. • Визуализация результатов и наличие артефактов, которые можно показывать бизнесу. Аргумент «Есть один нюанс.» • Нужен «MockServer» для инфраструктуры UI-тестирования. !11
  5. Server-side !13 Как это выглядит? • Разворачивается локальный сервер на

    билд-машине и у каждого разработчика, если он ему необходим для работы. • В коде приложения указывается локальный адрес хоста для доступа.
  6. Server-side !15 Почему отказались? • Разворачивается локальный сервер на билд-машине

    и у каждого разработчика, если он ему необходим для работы. • Необходимо поддерживать изменения API на клиенте и на сервере. • Имеем «глупый» сервер: request -> response.
  7. Client-side !16 Как это выглядит? • Можно использовать 3rd party:

    OHTTPStubs, Swifter, Embassy. • Набор возможностей, принцип работы и способ интеграции в приложение у каждого решения индивидуальны.
  8. Client-side !17 • У каждого решения свои плюсы и минусы.

    • Где были возможности использования «в рамках разработки» и «в рамках тестирования», там были проблемы с тем, что на выходе получали всё тот же «глупый» сервер. • Где можно было создавать сложные правила, там были неприятные моменты с интеграцией в проект. • Необходимо было бы разбираться в кишках этих решений. Почему отказались?
  9. Собственная реализация URLProtocol !20 1 2 3 4 5 An

    abstract class that handles the loading of protocol-specific URL data. open class URLProtocol: NSObject
  10. Реализация абстрактных методов !21 1 2 3 4 5 class

    func canInit(with request: URLRequest) -> Bool Return value true if the protocol subclass can handle request, otherwise false.
  11. Реализация абстрактных методов !22 1 2 3 4 5 class

    func canonicalRequest(for request: URLRequest) -> URLRequest Returns a canonical version of the specified request. It is up to each concrete protocol implementation to define what “canonical” means. A protocol should guarantee that the same input request always yields the same canonical form.
  12. Реализация абстрактных методов !23 1 2 3 4 5 func

    startLoading() When this method is called, the subclass implementation should start loading the request, providing feedback to the URL loading system via the URLProtocolClient protocol.
  13. Реализация абстрактных методов !24 1 2 3 4 5 func

    startLoading() // Работаем с self When this method is called, the subclass implementation should start loading the request, providing feedback to the URL loading system via the URLProtocolClient protocol.
  14. !25 1 2 3 4 5 var client: URLProtocolClient? {

    get } Работа с сущностью, в которую пихаем ответы
  15. !26 var client: URLProtocolClient? { get } The object the

    protocol uses to communicate with the URL loading system. Работа с сущностью, в которую пихаем ответы 1 2 3 4 5
  16. !27 configuration.protocolClasses = [MockServerURLProtocol.self] An URLSessionConfiguration object defines the behaviour

    and policies to use when uploading and downloading data using an URLSession object. Установка URLProtocol в URLSessionConfiguration 1 2 3 4 5
  17. Установка URLProtocol в URLSessionConfiguration !28 configuration.protocolClasses = [MockServerURLProtocol.self] URLSession objects

    support a number of common networking protocols by default. Use this array to extend the default set of common networking protocols available for use by a session with one or more custom protocols that you define. 1 2 3 4 5
  18. NetResponseRecorder !31 Создавать .json’ы ответов руками слишком долго — это

    можно оптимизировать, сохраняя локально весь трафик клиент-серверного общения. Помогает при поддержке новых версий API.
  19. NetResponseRecorder !32 Создавать .json’ы ответов руками слишком долго — это

    можно оптимизировать, сохраняя локально весь трафик клиент-серверного общения. Помогает при поддержке новых версий API. На самом деле, можно оптимизировать и эту автоматизацию.
  20. Первый результат Плюсы • Контроль сетевого взаимодействия. • MockServer стало

    возможным использовать в рамках разработки. Минусы • Всё работало в рамках одного окружения: ответы на запросы всегда приходили одни и те же. • Использование MockServer’а для прогона тестов не представлялось возможным. !35
  21. Файл-конфигурации .json !36 [ { "http_method": "GET", "request_method": "promo-actions", "stub_file_path":

    "promo-actions/GET_promo-actions_Transfer_RURU", "response_status_code": 200 }, { "http_method": "GET", "request_method": "countries", "stub_file_path": "countries/GET_countries_Online_In", "response_status_code": 200 }, { "http_method": "GET", "request_method": "events", "stub_file_path": "events/GET_events_Transfer_RURU", "response_status_code": 200 }, … ]
  22. Правило из конфигурации !42 { "http_method": "GET", "request_method": "countries", "stub_file_path":

    "countries/GET_countries_Online_In", "response_status_code": 200 } На запрос GET /countries необходимо прислать ответ, который находится по пути "countries/GET_countries_Online_In" с кодом 200.
  23. .bundle для MockServer’a !43 base-api configurations extra-api Дефолтные файлы-ответы на

    все запросы приложения. Конфигурации с наборами правил для MockServer’a. Файлы-ответы, которые указаны в конфигурациях, находятся в extra-api. Файлы-ответы, которые были указаны в конфигурациях — «перезаписывают» дефолтные ответы.
  24. !47 private let app = XCUIApplication() func launch(with configurationFilePath: String?

    = nil) { self.app.launchEnvironment[“KEY_1”] = “EMULATE” if let filePath = configurationFilePath { self.app.launchEnvironment[“KEY_2”] = filePath } self.app.launch() } BaseTest .swift
  25. !48 private let app = XCUIApplication() func launch(with configurationFilePath: String?

    = nil) { self.app.launchEnvironment[“KEY_1”] = “EMULATE” if let filePath = configurationFilePath { self.app.launchEnvironment[“KEY_2”] = filePath } self.app.launch() } Запуск тестов на эмулированных данных
  26. !49 private let app = XCUIApplication() func launch(with configurationFilePath: String?

    = nil) { self.app.launchEnvironment[“KEY_1”] = “EMULATE” if let filePath = configurationFilePath { self.app.launchEnvironment[“KEY_2”] = filePath } self.app.launch() } Передача параметра-конфигурации
  27. !50 private let app = XCUIApplication() func launch(with configurationFilePath: String?

    = nil) { self.app.launchEnvironment[“KEY_1”] = “EMULATE” if let filePath = configurationFilePath { self.app.launchEnvironment[“KEY_2”] = filePath } self.app.launch() } Передача параметра-конфигурации
  28. !51 1. Унаследовать каждый UI-тест от BaseTest. 2. Сказать приложению,

    что нужна эмуляция. 3. Создать файлы-ответы и файл конфигурации (при необходимости). 4. Передать в приложение имя файла конфигурации (при наличии). Суммируя: со стороны UITests-таргета
  29. !53 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Нужно ли активировать MockServer?
  30. !54 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Нужно ли активировать MockServer?
  31. !55 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Запуск произошёл из UI-тестов ProcessInfo.processInfo.environment
  32. !56 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Активация для штатного запуска
  33. !57 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Активация для штатного запуска needEmulateServer: Bool needUseConfiguration: Bool configurationName: String recordResponses: Bool EmulationConfig
  34. !58 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Все остальные случаи
  35. !59 func needEmulate(emulationConfig: EmulationConfig?) -> Bool { #if DEBUG if

    let value = environment[“KEY_1”], value == “EMULATE” { return true } else if let config = emulationConfig { return config.needEmulateServer } #endif return false } Защита от попадания в production
  36. !62 let configurationFileName: String? = { if let fileName =

    environment["KEY_2"] { // Файл с конфигурацией, который мы указали в UI-тесте. return fileName } else if emulationConfig.needUseConfiguration { // Файл с конфигурацией для штатного запуска приложения. return emulationConfig.configurationName } else { return nil } }() Запуск приложения
  37. !63 let configurationFileName: String? = { if let fileName =

    environment["KEY_2"] { // Файл с конфигурацией, который мы указали в UI-тесте. return fileName } else if emulationConfig.needUseConfiguration { // Файл с конфигурацией для штатного запуска приложения. return emulationConfig.configurationName } else { return nil } }() Запуск приложения
  38. !64 let configurationFileName: String? = { if let fileName =

    environment["KEY_2"] { // Файл с конфигурацией, который мы указали в UI-тесте. return fileName } else if emulationConfig.needUseConfiguration { // Файл с конфигурацией для штатного запуска приложения. return emulationConfig.configurationName } else { return nil } }() Запуск приложения
  39. !65 let configurationFileName: String? = { if let fileName =

    environment["KEY_2"] { // Файл с конфигурацией, который мы указали в UI-тесте. return fileName } else if emulationConfig.needUseConfiguration { // Файл с конфигурацией для штатного запуска приложения. return emulationConfig.configurationName } else { return nil } }() Запуск приложения needEmulateServer: Bool needUseConfiguration: Bool configurationName: String recordResponses: Bool EmulationConfig
  40. !66 let configurationFileName: String? = { if let fileName =

    environment["KEY_2"] { // Файл с конфигурацией, который мы указали в UI-тесте. return fileName } else if emulationConfig.needUseConfiguration { // Файл с конфигурацией для штатного запуска приложения. return emulationConfig.configurationName } else { return nil } }() Запуск приложения
  41. !67 print("MockServer: Получен файл конфигурации: '\(filePath)'.") // Получаем сконфигурированные правила

    из файла конфигурации. let parser = JSONResponseStrategiesParser() let rules = parser.rules(from: filePath) // Добавляем все созданные правила на MockServer. rules.forEach { MockServerProcessingDirector.shared.add(rule: $0) } Конфигурация MockServer’a
  42. !68 print("MockServer: Получен файл конфигурации: '\(filePath)'.") // Получаем сконфигурированные правила

    из файла конфигурации. let parser = JSONResponseStrategiesParser() let rules = parser.rules(from: filePath) // Добавляем все созданные правила на MockServer. rules.forEach { MockServerProcessingDirector.shared.add(rule: $0) } Конфигурация MockServer’a
  43. !69 print("MockServer: Получен файл конфигурации: '\(filePath)'.") // Получаем сконфигурированные правила

    из файла конфигурации. let parser = JSONResponseStrategiesParser() let rules = parser.rules(from: filePath) // Добавляем все созданные правила на MockServer. rules.forEach { MockServerProcessingDirector.shared.add(rule: $0) } Конфигурация MockServer’a
  44. !70 print("MockServer: Получен файл конфигурации: '\(filePath)'.") // Получаем сконфигурированные правила

    из файла конфигурации. let parser = JSONResponseStrategiesParser() let rules = parser.rules(from: filePath) // Добавляем все созданные правила на MockServer. rules.forEach { MockServerProcessingDirector.shared.add(rule: $0) } Конфигурация MockServer’a
  45. !71 print("MockServer: Получен файл конфигурации: '\(filePath)'.") // Получаем сконфигурированные правила

    из файла конфигурации. let parser = JSONResponseStrategiesParser() let rules = parser.rules(from: filePath) // Добавляем все созданные правила на MockServer. rules.forEach { MockServerProcessingDirector.shared.add(rule: $0) } Конфигурация MockServer’a
  46. !72 1. Нужно ли активировать эмуляцию? Если да, то: 2.

    Нужно ли использовать кастомную конфигурацию? Если да, то: 3. Получить правила из файла конфигурации. 4. Добавить правила из пункта 3 на MockServer. Суммируя: со стороны Application-таргета
  47. Итоги первого рефакторинга Плюсы • MockServer стало возможным использовать как

    для разработки, так и для автоматического тестирования. • У каждого UI-теста появилась возможность указать индивидуальную конфигурацию (конфигурации можно переиспользовать и в рамках нескольких тестов). Минусы • #if-директива DEBUG !73
  48. Что значит «нужна динамика»? !77 Курс доллара: 1. Пользователь хочет

    отправить деньги. 2. Уточняется курс (GET /tariffs). 3. …
  49. Что значит «нужна динамика»? !78 Курс доллара: 1. Пользователь хочет

    отправить деньги. 2. Уточняется курс (GET /tariffs). 3. … 4. Опять уточняется курс (GET /tariffs).
  50. Файл-конфигурации .json !79 { "static": [ … ], "dynamic": [

    { "http_method": "GET", "request_method": "tariffs", "responses": [ { "stub_file_path": "response_1", "response_status_code": 200 }, { "stub_file_path": "response_2", "response_status_code": 200 }, ] }, ] }
  51. Правило динамической обработки !80 { "http_method": "GET", "request_method": "tariffs", "responses":

    [ { "stub_file_path": "response_1", "response_status_code": 200 }, { "stub_file_path": "response_2", "response_status_code": 200 }, ] }
  52. Правило динамической обработки !81 { "http_method": "GET", "request_method": "tariffs", "responses":

    [ { "stub_file_path": "response_1", "response_status_code": 200 }, { "stub_file_path": "response_2", "response_status_code": 200 }, ] }
  53. Правило динамической обработки !82 { "http_method": "GET", "request_method": "tariffs", "responses":

    [ { "stub_file_path": "response_1", "response_status_code": 200 }, { "stub_file_path": "response_2", "response_status_code": 200 }, ] }
  54. MockServerProcessingDirector !84 1. Контейнер для всех правил: После парсинга файла-

    конфигурации, все правила устанавливаются именно сюда. Хранятся в массиве, упорядоченном по убыванию приоритетов этих правил. 2. Первое звено в обработке какого-либо реквеста: именно сюда прилетает перехваченный URLProtocol для каждого запроса.
  55. !87 = Priority + Aspect MockServerRule Основная характеристика правила, по

    которой понимаем, что нужно делать: static, dynamic, serverUnavailable, etc.
  56. Стратегии для обработки !88 В зависимости от аспекта правила создаётся

    стратегия обработки. Тут могут находится сервисы по формированию имени файлов, по извлечению данных, по отправлению ответов на клиентскую часть — MockServerResponseTransmitter.
  57. MockServerResponseTransmitter !89 func sendSuccess(data: Data, statusCode: Int, urlProtocol: URLProtocol) {

    … let url = urlProtocol.request.url! let response = HTTPResponse(url, statusCode, httpVersion, headerFields)! let client = urlProtocol.client! client.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(urlProtocol, didLoad: data) client.urlProtocolDidFinishLoading(urlProtocol) }
  58. MockServerResponseTransmitter !90 func sendSuccess(data: Data, statusCode: Int, urlProtocol: URLProtocol) {

    … let url = urlProtocol.request.url! let response = HTTPResponse(url, statusCode, httpVersion, headerFields)! let client = urlProtocol.client! client.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(urlProtocol, didLoad: data) client.urlProtocolDidFinishLoading(urlProtocol) }
  59. MockServerResponseTransmitter !91 func sendSuccess(data: Data, statusCode: Int, urlProtocol: URLProtocol) {

    … let url = urlProtocol.request.url! let response = HTTPResponse(url, statusCode, httpVersion, headerFields)! let client = urlProtocol.client! client.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(urlProtocol, didLoad: data) client.urlProtocolDidFinishLoading(urlProtocol) }
  60. MockServerResponseTransmitter !92 func sendSuccess(data: Data, statusCode: Int, urlProtocol: URLProtocol) {

    … let url = urlProtocol.request.url! let response = HTTPResponse(url, statusCode, httpVersion, headerFields)! let client = urlProtocol.client! client.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(urlProtocol, didLoad: data) client.urlProtocolDidFinishLoading(urlProtocol) }
  61. MockServerResponseTransmitter !93 func sendSuccess(data: Data, statusCode: Int, urlProtocol: URLProtocol) {

    … let url = urlProtocol.request.url! let response = HTTPResponse(url, statusCode, httpVersion, headerFields)! let client = urlProtocol.client! client.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(urlProtocol, didLoad: data) client.urlProtocolDidFinishLoading(urlProtocol) }
  62. MockServerResponseTransmitter !94 func sendFailure(error: NSError, urlProtocol: URLProtocol) { let client

    = urlProtocol.client! client.urlProtocol(urlProtocol, didFailWithError: error) }
  63. MockServerResponseTransmitter !95 func sendFailure(error: NSError, urlProtocol: URLProtocol) { let client

    = urlProtocol.client! client.urlProtocol(urlProtocol, didFailWithError: error) }
  64. Итоги всех рефакторингов Плюс • Индивидуальные конфигурации, которые включают в

    себя правила со статическими ответами, с динамическими ответами, а также специфические правила: 503 ошибка для определённых запросов и т.п. !96
  65. Итоги всех рефакторингов Небольшой тюнинг • DEBUG в #if-директиве заменился

    на свою собственную переменную. • Выглядит немного лаконичнее. !100
  66. Итоги всех рефакторингов Немного самокритики • Динамика прописывается в файле

    конфигурации. • Было бы здорово прописывать динамику в самом тесте, непосредственно перед тем, как нужно подменить ответ. !101
  67. Планы на будущее? • Новые правила. • Попытки сделать конфигурации

    ещё проще. • Вынесение динамики в тесты. • Возможно, что-то ещё — время покажет. !102