Часто слышу мнение, что unit-тесты не нужны для мобильной разработки: в приложении должно быть минимум логики, основная работа с UI, а его сложно тестировать, да ещё и тесты отнимают время, которое можно было бы потратить на написание фич. 

За этим мнением скрывается простая правда — люди, которые так говорят, не умеют писать тесты. Не умеют писать их быстро; писать там, где нужно; писать так, чтобы была ощутимая польза для бизнеса. Я тоже был таким — понимал, что тесты нужны, но не понимал какие, где и как их писать. 

Рассказываю, что поменялось спустя 2 года и 4 тысячи тестов.

В 2018 я проходил собеседование в Додо Пиццу, общался с СТО. Всё шло хорошо, мы подвели итог и уже начали прощаться, но тут Саша прерывает:

— Стой, стой, не клади трубку. А тесты пишешь?

Я растерялся, потому что даже не понял вопрос. Что за тесты? «Не страшно, научим», — сказал Саша, и мне пришёл офер через пару дней. 

В первые месяцы вместе со вторым iOS-разработчиком Лёшей я записался на Dev school внутри компании. Уроки проходили на С#: я плохо понимал, но у Лёши уже был опыт на этом языке, поэтому домашку мы делали вместе в офисе рано утром. В школе были строгие правила по сдаче домашке: одну мы сдали не в 9 утра понедельника, а в 9:05 и нас отчислили. «Научить» не получилось :D

Тем не менее, первые уроки были круты и их хватило, чтобы задать вектор для самостоятельного ресёрча. Было понятно, что вдолгую тесты писать точно надо: мы работаем в продуктовом проекте, развивать его будем долго, впереди несколько десятков стран со своими особенностями. 

Иногда мы делали попытки писать тесты, но было очень много вопросов, процесс шёл с трудом и постоянно вяз. Давайте разберём типичные вопросы про тестирование, которые нам мешали.

Почему в мобилке нет тестов? UI-first

Только 1 из 20 кандидатов писал какие-то тесты, причём чаще это были UI-тесты. В маленьких компаниях их не пишет никто, не слышали про них и не видят ценности.

Чаще всего фича начинается с UI, первые проверки выполняются вручную. Потом надо дописать немного логики в домене, это тоже проверяется руками по привычке. Если в какой-то момент и захочется эту проверку автоматизировать, то… до конца фичи осталось немного, я ещё руками потестирую. Profit!

Зачем они нужны

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

Проблемы бывают и на приёмочном тестировании задачи. Иногда оно превращается в пин-понг:

  • разработчик что-то сделал и отдал QA;

  • тот проверил и вернул назад;

  • разработчик поправил второй баг и отдал QA;

  • оказывается, теперь предыдущая штука не работает;

  • начинаем круг заново. 

В обоих случаях проблема одна — разработчик меняет код, но о проблеме узнает спустя время. И чем больше проходит времени — тем хуже для скорости решения задачи.

Как тесты влияют на релизы

Загвоздка в том, что если мы уже сделали фичу, то задумываться обо всех этих вопросах становится поздно. Если начать писать тест, отделять зависимость и писать вспомогательный код, то придётся менять всё. Сломать легко, а если задачу уже проверил QA, то не хочется его просить второй раз.

Можно «перевернуть игру»: сначала написать тест, зафиксировать требования от бизнеса, которое хотим реализовать. Потом уже писать код, который пройдёт проверки. Так мы будем наращивать требования к программе, и если надо будет её изменить, то легко это сделаем: когда что-то сломается — тест упадёт и мы узнаем, о чём забыли. 

Кайф в том, что таких проверок может быть сотни в одну секунду.

На весь проект можем написать несколько десятков тысяч тестов и узнавать о том, что вся программа работает правильно, за несколько минут.

В идеале, если тесты написаны хорошо, проверяют самое важное и мы им доверяем, то после их выполнения можем выпускать новую версию программы. Если программа всегда стабильна, то можем релизить её чаще: быстрее давать новые фичи людям, получать обратную связь и всё такое. А ещё можем не бояться менять приложение: если что-то сломали, то сразу и починим. Всем хорошо.

Итак, тесты — это хорошо. Но…

7 отговорок

Все понимают, что писать тесты — хорошо, но реальность была иной. Я поспрашивал у разработчиков, какие есть причины не писать тесты.

  • Продакт не выделяет на это время.

  • Сложно тестировать ту фичу, которую сейчас разрабатываю.

  • Надо много мокать, это скучно.

  • Честно — лень.

  • Тесты должны писать QA.

  • Нам платят не за тесты. 

  • И вообще это ничего не даст.

Однако эти «возражения» легко перебивались разумными доводами.

  • Продакт времени не даёт? Он не компетентен оценивать сроки и не отвечает за качество кода, это работа программиста, а тесты — это всего лишь один из рабочих инструментов, который можно применять. 

  • Сложно протестировать код? Надо только начать, дальше будет проще. Скорее всего, протестировать нужно не всё, а лишь самую важную часть.

  • Слишком много мокать? Возьми модуль покрупнее и интерфейс попубличнее — не надо тестировать каждый класс в изоляции, отделять всё подряд в интерфейсы и генерировать для этого тест-дублёры. Сосредоточься на ключевых зависимостях, которые хочешь контролировать: UI, сеть, натройки телефона, результаты предыдущего взаимодействия с программой.

  • Лень? Но тестировать релиз руками ещё ленивее. Если вкладываться каждый день по чуть-чуть, то не надо будет страдать на регрессионном тестировании. 

  • Тесты должны писать QA? Они могут написать, но это будут самые медленные и нестабильные UI-тесты. Обратная связь от таких тестов будет приходить поздно, проверять смогут только самые верхнеуровневые сценарии. А ещё у нас мало QA.

  • За тесты не платят? Платят за разработку качественного продукта, тесты —  это часть качества.

  • Это ничего не даст? Наш проект живёт уже несколько лет, прошлые тесты сейчас очень пригодились бы.

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

Писать тесты мы не умели.

Как учились

Пойти и с наскока писать тесты в большом проекте очень сложно, поэтому мы сделали шаг назад и начали решать искусственные задачки для отработки навыков тестирования — TDD-каты. Внутри каты простенькая алгоритмическая задачка, цель — научиться сначала писать тесты, а уже потом писать код. 

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

Классический пример unit-теста.

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

Как начинали

Весной 2020 мы заканчивали переделку оформления заказов. Функциональность оказалась нетривиальной — десятки пограничных кейсов сходятся в одном месте. Ребята очень старались, но какие-то задачи начали раз за разом возвращаться от QA: мы это чинили месяц назад, а оно снова сломалось. Тесты не писали для «ускорения» разработки, но от этого разработка завязла, исправляя одно и тоже по кругу. 

В мае мы поняли, что дальше без постоянных тестов никак: у нас десяток стран, в них бывает разное поведение, появляются разные фича-тоглы, приложение решает тысячи мини-задач. И всё должно работать стабильно у каждого пользователя.

Первое осознанное решение — решили писать и проговорили это со всеми командами. Мы не знали, сколько понадобится времени и сил, не представляли как оценить, но точно знали, как нам выбираться из ситуации. Интересно, что в следующие два года никто даже не поднимал вопрос о том, что можно отказаться от тестов ради скорости разработки, а вот код без тестов вызывает много вопросов.

Нужно было привить привычку писать тесты регулярно. Решили говорить об этом постоянно: завели метрики на количество тестов и стали публиковать их в общем чате мобилки раз в неделю.

В среднем писали по 30 unit-тестов в неделю — сначала стартанули быстро и выходило около 100, по 20-25 тестов на человека, но со временем замедлились и текущий темп около 30-40 тестов. Если темп замедлялся, то слегка рефлексировали над причинами и помогали друг другу. В проекте было несколько активных ребят, которые помогали писать тесты менее активным и отвечали на их вопросы.

Принципы

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

Самое сложное — написать первый тест. Это нужно принять и соответственно закладывать время для решения задачи. Я делал так: настраивал все окружение, чтобы написать самый простой сценарий основного пути. После первого теста было намного легче протестировать поведение рядом с ним. 

Баг — это ненаписанный тест. Если подумать, то причина бага кроется в том, что мы что-то не учли, потому что система стала слишком сложной или обстоятельства мешали, или другие требования противоречили. Но всё приводит к тому, что разработчик теряет контроль над кодом и ему нужны дополнительные инструменты, чтобы его вернуть. Тесты помогают. Так вы будете быстро получить фидбек, задача всегда будет двигаться только вперёд и не нужно будет чинить одно место раз за разом. 

Для исправления бага нужно время. Бывали случаи, когда попадался простой баг, а мы— в ментальную ловушку: разработчик на простое исправление выделял минут 10 и баг закрывался, а тесты «забывались». Договорились, что на исправление даже небольших мест должны себе выделять час времени — так можно и отрефакторить, и тест написать, и рядом что-нибудь покрыть. Если баг случился, то нужно не просто устранить проблему, но и подумать, как поменять код, чтобы в следующий раз подобную ошибку не допустить.

Детальнее можно почитать у Никиты Тонского: зачем, что и как тестировать.

Технологии

Стандартных XCTest быстро стало недостаточно, и мы подключили ещё несколько фреймворков.

Тесты на поведение

Все зависимости класса отделяли протоколом, для него писали моки на Spry, а потом писали тесты. Некоторые проблемы решались этим хорошо, но появлялось много дополнительного кода в виде интерфейсов и моков. «Налог» на такой подход довольно большой, многие начинают подключать автогенерацию кода. Мне такой подход не нравится, но об этом в другой раз.

Quick и формат спецификаций. 

В дополнении к XCTest мы взяли фреймворк Quick. Понравилось, что можно писать связанные сценарии пользователя, проще именовать тест, лучше отделены шаги.

Quick-тесты легко писать, но сложно поддерживать в долгую: из-за большого количества замыканий код компилируется медленнее, сам фреймворк хуже интегрирован в Xcode, от этого прогон тестов тоже получается медленнее.

В итоге пришли к правилу, что если можно написать на XCTest, то лучше написать на нём, но чаще всего пишем на Quick.

Скриншот-тесты

В мобильных приложениях много приходится работать с UI, а он очень вариативный: разные состояния, разные модели телефонов. 

У нас был процесс дизайн-приёмки, когда на этапе тестирования результат работы проверял не только QA, но и дизайнер. Этот этап был нудный, потому что нужно было ручками перебрать все возможные варианты вёрстки, а любое исправление перезапускало весь процесс. Чувствуете, к чему клоню? :-)

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

Для тестирования мы взяли библиотеку SnapshotTesting от Point-free, потому что её можно было использовать вместе с Quick.

Из подобных тестов можно получить полный набор состояний:

Карточка товара в корзине и 19 ее состояний

Применять такой подход можно не только к UIView, но и к другим частям приложения. Например, делать снимок данных с описанием сетевых запросов, события аналитики, описания доступности и т.п. Сам подход сравнения текущих результатов работы с предыдущими называется Golden-master тестированием. Такие тесты могут помочь с забористым легаси: вы фиксируете текущее состояние работы системы, а потом его не ломаете. 

UI-тесты и проект автоматизации

Для упрощения наши QA уже писали UI-тесты — около 100 штук. Это снимало часть ручной работы, помогало проверить приложение перед релизом. Для стабилизации тестов ребята подменяли ответы от бэкенда и читали их из файлов. 

В один момент оба QA уволились, экспертизы у разработчиков не было, сетевой контракт поменялся, фикстуры ответов от сервера стали невалидны и тесты зачахли. 

Новый QA только пришёл в автоматизацию, ему было проще начать писать новые тесты, которым он может доверять. В какой-то момент тесты стали сильно зависеть от работы сети: она нестабильна и довольно медленна. Ему тоже захотелось подменять ответы для ускорения прогона, но мы уже проходили этот путь и второй раз на те же грабли не хотелось. 

Нужно был инструмент, который бы стал прокси между приложением и сервером, мог записать все сообщения между ними и подменить по необходимости.

Мы собрали команды из мобильных разработчиков и QA-автоматизаторов, они взяли Chuck и допилили его под наши задачи. Все ответы сервера складывали в репозиторий, прокси запускали на локальной машине, от чего получили высокую стабильность и скорость. Если контракт обновится, то  удалим старые фикстуры, прогоним тесты относительно стенда, запишем новый контракт, ручкам поменяем несколько значений для негативных кейсов и всё снова начнёт работать. 

Сейчас работаем над тем, чтобы передать поддержку и написание новых UI-тестов в команды. Так у всех будет экспертиза, тесты будут живыми и разработчик сам сможет решить, на каком уровне тестировать фичу. Важно не перегнуть и не написать чересчур большое количество UI-тестов, они слишком нестабильны. Пишите тесты как можно ниже в пирамиде тестов.

До июня 2020 мы не измеряли регулярно количество UI-тестов.

Насколько тесты помогли

Спустя пару лет у нас много разных тестов. Пока нет какого-то модуля, про который можно было бы сказать, что он отлично протестирован — например, даже в самых покрытых местах test coverage едва переваливает за 50%. 

Важный вопрос: насколько разные тесты помогли в работе и ускорили?

  • Unit-тесты упростили некоторые задачи: меньше условий приходится держать в голове, стало проще проверять поведение в негативных кейсах, легче передавать задачу от одного разработчика к другому.

  • Скриншот-тесты сильно упростили этап «дизайн-приёмки»: мы скидываем дизайнеру получившиеся варианты, вёрстка перестала «разваливаться» со временем, когда новые изменения что-то ломали в одном из вариантов. 

  • UI-тесты сократили ручные проверки перед релизом: сейчас 70% кейсов проверяются меньше чем за два часа. Остальные 30% занимают минимум по 4 часа как на iOS, так и на Android.

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

Два года назад у нас стабильно на каждую версию приложения выходило по 2-4 фикса, а сейчас редко когда выпускаем больше одного. На это повлияли и другие задачи, например, когда мы отказались от Realm, но заслуга тестов точно есть. 

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