Привет, я Стас, лид мобильной разработки Туту.ру. Хочу поделиться, к чему мы пришли, к чему только идём, а от чего избавились за пять лет, что я в компании. Часть решений может шокировать. Поехали!

Как мы пришли к снапшот-тестам и Data-Driven View Controller

Мы заморачиваемся тестированием и его автоматизацией. Был собран практически полный автоматизированный прогон, для релиза надо получить зелёные тесты и сделать покупку с тестовой карты. С точки зрения видов тестов, помимо стандартных Unit и UI, мы пришли к snapshot-тестированию.

Snapshot-тестирование – это

когда тест рендерит вью, делает скриншот и сравнивает его с референсной картинкой. Это дешевый способ убедиться, что верстка не поехала.

Но так было не всегда, довольно долго в DoD задачи были только unit-тесты. Но в один прекрасный день мы провели внутренний хакатон, посвящённый качеству. У него было две цели: научить разработчиков писать UI-тесты и проработать обвязку в виде хелперов, опенеров и моков сетевых запросов. На том же хакатоне «на сдачу» затащили snapshot-тестирование.

class PassengersViewControllerTests: XCTestCase {
    func testPassengersViewControllerLoadedState() {
        let vc = _makePassengersViewController(.loaded(.mock))
        assertSnapshots(matching: vc, as: .images(.config))
    }

    func testPassengersViewControllerEmptyStateLogined() {
        let vc = _makePassengersViewController(.empty(.logined))
        assertSnapshots(matching: vc, as: .images(.config))
    }

    func testPassengersViewControllerEmptyStateNotLogined() {
        let vc = _makePassengersViewController(.empty(.notLogined))
        assertSnapshots(matching: vc, as: .images(.config))
    }

    func testPassengersViewControllerLoadingState() {
        let vc = _makePassengersViewController(.loading)
        assertSnapshots(matching: vc, as: .images(.config))
    }
}

Писать хорошие снапшотные тесты нам помогает  подход Data-Driven View, которого мы придерживаемся на обеих платформах, — его мы подсмотрели в докладе Алексея Демедецкого. Решение намазывается на любую архитектуру, попробуйте, если ещё нет.

Спойлер

По теме можно делать отдельную статью, но лучше посмотреть выступление Алексея

Если кратко, идея вот в чём: у вью, особенно в архитектурах с презентерами, часто делают широкий интерфейс. Функция на обновление шапки таблицы, на саму таблицу, на разные состояния. Думаю, вы неоднократно видели экраны, которые находятся в неконсистентном состоянии. На половине экрана — ошибка, на половине — устаревшее состояние. Если сделать интерфейс вью с единственной функцией render(with: Model), это нас с одной стороны заставит заранее готовить консистентную модель, с другой — упростит рендеринг, из-за того, что у нас есть все данные для отображения. В качестве бонуса получаем возможность писать снапшот/скриншот-тесты.

От зоопарка архитектур к RxFeedback — и обратно

У нас есть несколько мобильных подкоманд, каждая отвечает за своё продуктовое направление: авиа, ж/д, автобусы. У каждого направления своё приложение и своя команда, а у каждой команды свои подходы — VIPER, MVVM, MVP. Около трёх лет назад у бизнеса появилась потребность объединить приложения, а как следствие, и команды под одним продакт-оунером.

Перед нами возникла необходимость выбрать единый архитектурный стандарт. На Android всё получилось довольно легко и органично, а в iOS возникла идея использовать Composable Architecture. Как всегда, всё началось с идеи одного из разработчиков: мы обкатали её на небольшом проекте и поняли, что с ней можно решать наши кейсы.  

Например, у нас была проблема с визардами: когда человек покупает билет, надо брать и заполнять одно состояние заказа на куче экранов. Просто чтобы был понятен масштаб: покупка билета с точки зрения объёма кодовой базы и числа экранов занимает две трети приложения. RxFeedback помогает нам легко шарить состояние на куче экранов. Это же и стейт-машина и она хорошо решает задачу обновления сложных состояний. 

В своё время мы выступали про это на CocoaHeads

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

От монорепозитория к доменам (солюшенам)

Всё началось с того, что один из разработчиков между делом сказал: «Вот я уже полтора года в компании, а всё ещё периодически попадаются части проекта, в которые я никогда не заглядывал». В августе 2020-го мы пошли на выделение отдельных команд, которые отвечают за конкретные домены и пользовательские сценарии. Каждая команда начинается с бизнесовой направленности, потом появляется продакт, а потом и вся остальная команда. То есть разбиение касалось не только мобильных разработчиков. В итоге у команд появилась постоянная зона ответственности, за которую они отвечают и могут итерационно улучшать.

В итоге у нас образовалось 4 вида команд:

  • Команды, отвечающие за приложения.

  • Команды, отвечающие за солюшены aka домены.

  • Проектная команда, которая приходит, делает сложный проект и идёт дальше.

  • Core — продумывают стандарты, проектируют, делают ревью почти всех, а, когда необходимо, приходят и помогают руками.

Первым отделили поисковую выдачу со всеми её фильтрами. Сейчас в такой же парадигме делаем отели. У них отдельные репозитории — ты владеешь всем кодом, периодически поднимая версию своей зависимости в репозитории приложений. Мы ещё в середине пути и продолжаем резать приложение, чтобы у каждой фича-команды была явная зона ответственности и она не была слишком большой.

Как мы готовимся к SwiftUI

В плане SwiftUI и Combine пришла беда откуда не ждали. До последнего года мы придерживались правила, что минимальная версия — это текущая минус две. Это обусловлено процентом пользователей, сидящих на конкретной оси. Где-то в районе 4-5% поднимаем вопрос об отказе от конкретной оси. Всех подставила iOS 12, доля пользователей на которой снижается в два раза медленнее, чем на любой другой оси. А в июле 2021-го пользователей на ней стало даже больше, чем на 13-й.

Наконец процент на iOS 12 упал достаточно, чтобы мы начали отказ от неё. Это даст нам возможность начать выпиливать довольно толстый RxSwift. SwiftUI планируем обкатывать в первую очередь в песочнице.

Благодаря Data-Driven View мы планируем переехать легко, ведь модели для рендеринга уже готовы. Правда сам первый SwiftUI обещает подкинуть проблем, но это уже совсем другая история.

Что не зашло или не стало обязательным

Конечно, за столько лет были и не взлетевшие истории. Из памятного — был заход в использование функциональных операторов: но мы свернули эту историю, так как она дико увеличивала порог входа.

Несмотря на требования по тестам на каждую задачу, TDD — является рекомендацией. Лично для меня умение применять этот подход — огромный плюс для разработчика, за который на том же собеседовании я готов простить многое. Сейчас на каждой из платформ активно используют TDD — 2-3 человека. Остальных мы не пушим. 

Что ещё у нас с технологиями

Кратко расскажу про концепцию техрадара. Всё что использует команда делится на 4 группы: инструменты, платформы, языки/фреймворки и подходы. Каждая группа занимает свой квадрант на радаре. Внутри квадранта у сущности в свою очередь есть 4 состояния: активно используется, пробуем, присматриваемся, отказались.

Техрадар это еще и правила перемещения между этими состояниями. Чтобы начать использовать технологию, надо вначале изучить её на тестовых примерах, затем попробовать на неважных экранах, а уже потом втаскивать в бою на основной флоу пользователя.

Наш техрадар

Комментарии (5)


  1. OlKir
    03.02.2022 17:08

    Спасибо за то что делитесь опытом!
    А можете коротко описать как правильно применять TDD в мобильной разработке?
    Я могу себе это предстваить когда речь идет о логике (создание той же модели, изменение ее состояний или покрытие API). Но неужели вы и для интерфейса сначала пишете UI тест который падает? Или вы проверяете генерацию UI в коде юнит-тестами?



    1. onsissond
      03.02.2022 17:26
      +3

      Привет! Мы применяем TDD при разработке систем (аля view model), для интеграционных тестов между слоями и на сами сервисы. Вместо view model мы уже несколько лет практикуем однонаправленные архитектуры, такие как RxFeedback и Composable Architecture. Одним из плюсов стейт машин в том, что их удобно разрабатывать в парадигме TDD. В плане сервисов TDD полезен при разработке парсеров, конвертеров, форматеров и тд.


  1. Murtagy
    03.02.2022 20:50

    Тех радар не грузится. Возможно потому что я выбрал только strictly necessary cookies.


  1. WildDuckFighter
    04.02.2022 17:50

    А расскажите, пожалуйста, если интерфейс вью с единственной функцией render(with: Model), то как рендерить ансихронные изменения данных?
    Например, идут 2 параллельных запроса на бэк, приходит ответ и результаты обоих надо отобразить во вьюшке.


    1. onsissond
      04.02.2022 20:18
      +1

      При разработке вью слоя, мы абстрагируемся от нижележащих слоев. Каждый вью компонент имеет свой ViewState, который задает конфигурацию для вью. Вью стейт описывает все возможные состояния вью. Функцию render дергается каждый раз когда меняется ViewState.

      В твоем случае когда прилетит первый ответ он смапится во viewState, дернется render и экран перерисуется, прилетит второй ответ, получаем новый вью стейт и еще раз дергаем render.