Какие есть тесты для айфона

Тестирование iOS-приложений

Тестовое приложение

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

Для удобства я выложил готовое приложение на гитхаб.

Модульные тесты

Для написания модульных тестов я использую замечательный инструмент — Cedar.
Он позволяет писать тесты в стиле RSpec, что улучшает структурированность и читабельность кода.

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

Для установки необходимо склонировать проект и запустить скрипт install.sh, Cedar будет установлен в /opt/cedar и будут добавлены символические ссылки в домашнюю директорию пользователя (для большего удобства при подключении в проекты).

После того как Cedar собран нужно настроить тестовый таргет.

  1. Добавьте к вашему проекту новый таргет (Empty Application), я назвал его UnitTests.
  2. Прилинкуйте Cedar к таргету (Link Binary With Libraries)
  3. Добавьте в Other Linker Flags -ObjC -all_load -lstdc++
  4. Удалите AppDelegate. Этого можно и не делать, но он нам не нужен.
  5. Отредактируйте main.m как показано ниже

Давайте напишем первый тест.
Создайте в UnitTests target файл FooBar.mm с таким содержанием:

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

Вернемся к калькулятору. Допустим у нас будет какой-нибудь класс-синглтон, который будет заниматься вычислениями, назовем его CalculationManager. У него должен быть метод который должен возвращать инстанс этого класса, назовем его sharedInstance.
Напишем тест на этот кейс.
Создайте пустой класс CalculationManager в вашем главном target’е и добавьте еще один файл для тестов (к примеру CalculationManager.mm) с таким содержимым:

При запуске видно что тест валится.
Добавьте реализацию чтоб тест проходил успешно и продолжим.

Добавим пару тестов на операции сложения и вычитания.

На этом все, будем считать что прилжение достаточно покрыто модульными тестами, и перейдем к тестированию интерфейса.

Тестирование интерфейса

Существует немало средств для тестирования интерйеса iOS приложений, но я хочу рассказать о тех которыми пользуюсь сам, а именно Calabash-iOS и Frank.

Эти инструменты очень похожи, они оба позволяют писать тесты на Cucumber’е и оба реализованы на Ruby, разница лишь в функционале.
В одном из проектов мне пришлось мигрировать с Frank’а, я просто запустил тесты с использованием Calabash’а и все они прошли почти сразу, пришлось только немного изменить пару шагов.

Сейчас я остановился на Calabash. Думаю что многие iOS разработчики не знакомы с Cucumber, потому и хочу немного рассказать как он работает и как писать тесты.

Cucumber

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

Итак, в Cucumber’е есть несколько главных «сущностей»:

Feature — это набор нескольких связанных по логике сценариев (или не связанных, уж как программист решит). Она состоит из названия и краткого, информативного описания. К примеру:

Scenario — конкретный сценарий описывающий некоторый use case. Состоит из имени и набора шагов.

Step — описание конкретного действия пользователя (нажатие на кнопку/ссылку, ввод текста, свайп и.т.п).

Step definition — реализация конкретного действия пользователя. Выглядит она примерно так:

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

Давайте добавим Calabash в наш проект.
Перейдите в деректорию с проектом и выполните следующие команды:

Calabash добавил к нашему проекту еще один таргет, по умолчанию он имеет шаблон project_name-cal. Нам необходимо выполнить build для этой цели.
Теперь мы почти готовы запустить тесты.
После генерации выводится подсказка как запустить тесты

но на выполнении этой команды все валится, т.к. calabash не знает где находится наше приложение. Для этого необходимо указать еще одну переменную — APP_BUNDLE_PATH. По умолчанию Xcode 4.x хранит приложения по адресу

/Library/Application\ Support/iPhone\ Simulator/x.x/Applications/hash/app_name.app

где x.x — версия iOS, а hash — сгенерированный Xcode’ом уникальный ключ для приложения.
Попробуйте найти свой .app и выполнить следующее

Теперь все должно пройти хорошо.

Guard

Такой способ не очень удобен, но он вполне оправдан, т.к. calabash не может знать где лежит наше приложение. И тут нам на помощь приходит Guard.
Guard — это гем, который следит за файловой системой и при изменении файлов, за которыми он следит выполняет какие-либо операции. Список guard’ов довольно обширный, но нам нужен guard-calabash-ios.

Для его установки и использования необходимо выполнить следующее:

Это создаст Guardfile — файл в котором описаны свойства необходимые guard’у и файлы за которыми нужно следить. (Детальные настройки можно найти на гитхабе.)
Последний штрих — откройте настройки Xcode и установите Derived Data как Relative. Теперь Xcode будет хранить сборки в директории с проектом, что позволит скрипту из guard-calabash-ios найти нужный нам APP_BUNDLE_PATH автоматически.
Теперь для прогона тестов необходимо выполнить в папке с проектом следующее.

Пишем тесты

Теперь когда все работает более удобно мы можем приступить к написанию наших UI-тестов.

Calabash создал папку features, в которой находятся наши сценарии и реализация шагов.
Давайте удостоверимся что наш калькулятор позволит пользователю сложить или вычесть два числа, и показать верный результат в alert view.

Отредактируйте файл my_first.feature

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

Итак, все тесты провалились, что логично.

Давайте добавим UI.

Для доступа к контролам из calabash’а нужно задать им accessibility label. Кроме того к кнопкам можно обращаться по надписи на них, а к текстовым полям по плэйсхолдеру.

Я сделал примитивный интерфейс: два текстовых поля и две кнопки в navigation bar’е, «+» и «-«.
После того как мы добавили контролы на наш экран нам нужно выполнить следующие действия:

1. Добавить outlet’ы для кнопок и текстовых полей
2. Задать плэйсхолдеры нашим текстовым полям «left» и «right»
3. Задать accessibility label’ы для кнопок

4. Повесить обработчики на наши кнопки

5. Добавить метод для отображения результата

6. Перейдите в терминал с запущенным guard’ом и нажмите Enter, это запустит все ваши сценарии, у нас он один и если вы все сделали правильно, то тесты пройдут успешно.

Теперь напишем тест для вычитания.

После запуска Cucumber сообщит что не знает таких шагов, и предложит их реализовать.
Скопируем и немного подредактируем то что он вывел в файл calabash_steps.rb (project_dir/features/steps_definitions/)

В реальной жизни мы скорее всего использовали бы теже методы что и в первом сценарии, но здесь я хотел показать как выглядят step definitions, как вызывать другие шаги из реализации шагов(step %<>), как добраться до какого-либо значения (query) и как писать assert (should).

На этом по тестам все.

Заключение

Описанные тесты и приложение выглядят совершенно нелепо, но я ставил своей целью описать на этом примере основные возможности, которые позволят сразу начать использовать TDD/BDD, надеюсь что это у меня вышло и для статья окажется полезной.

Источник

Тесты в iOS: хороший, плохой, …

Привет! Меня зовут Андрей Михайлов, я работаю iOS-разработчиком в Циан и отвечаю за внедрение модульного тестирования в процесс разработки. Сегодня я немного расскажу о том, какими свойствами должны обладать хорошие автоматизированные тесты, чем хорошие тесты отличаются от плохих, и для чего на самом деле стоит их писать (не только чтобы находить баги в коде). Это первая статья в серии, посвященной тестированию, она сфокусирована на теории и будет полезна не только iOS-разработчикам. Статью, посвященную UI-тестированию, можно прочитать тут.

Читайте также:  Как почистить от пыли разъем для зарядки айфон

Предыстория

Начну с небольшой предыстории. Изначально в iOS приложении Циан был классический VIPER с отдельными наборами Unit-тестов на Presenter, Interactor и прочую бизнес-логику, уровнем ниже UI. Долгое время это было стандартом тестирования, но написание таких тестов было довольно трудоёмкой задачей. Они требовали создания большого количества моков и дублирования проверок одних и тех же сценариев на разных слоях. Постепенно эти тесты стали разрабатываться от случая к случаю, а качество их стало страдать.

Параллельно с Unit-тестами отдельная команда автоматизаторов разрабатывала Appium UI-тесты. Но с ними тоже хватало проблем. Appium далеко не сразу начинал поддерживать актуальную версию Xcode, а сами Appium тесты писались с задержкой в несколько недель после релиза фич, и когда автоматизаторы добирались до их написания, сталкивались с тем, что элементы на экране требуют проставления дополнительных идентификаторов. В результате разработчику приходилось возвращаться к задаче, что несколько выбивало из ритма работы.

В итоге было решено кардинально изменить эту ситуацию. Сначала были внедрены нативные UI-тесты (готовим отдельную статью про них). Теперь каждая продуктовая задача, чтобы пройти ревью, должна была содержать UI-тест, написанный разработчиком. Но в ходе разработки мы обнаружили несколько проблем UI-тестов — их тоже сложно писать, они долго выполняются, не все удается проверить (например, неудобно тестировать аналитику), а инструментарий от Apple далеко не идеален. Поэтому мы решили попробовать писать модульные тесты в дополнение к UI-тестам, чтобы сократить количество сценариев и проверок в UI-тестах.

Сразу пару слов про различие модульных и Unit-тестов, так как это довольно дискуссионный вопрос. Unit-тестом мы считает тест для одного класса. Желательно, чтобы у класса, который покрывается Unit-тестами, вообще не было никаких изменяемых зависимостей, тогда его будет легко покрыть Unit-тестами. А модульными тестами мы считаем те, которые тестируют связку классов, где количество заменяемых моками сущностей модуля должно быть минимально возможным.

Почему же мы выбрали модульные тесты, а не классические Unit-тесты для VIPER? Во-первых, модульные тесты позволяют протестировать всю систему целиком, включая связи между частями модуля. В результате получим лучшее покрытие с меньшим количеством тестов, а как известно в VIPER много отдельных сущностей и, соответственно, тестов для них. Во-вторых, модульные тесты требуют меньше моков и меньше кода на этапе подготовки теста, а значит и усилий разработки. А в-третьих, такие тесты удобно писать прямо по пользовательским историям, и они выступают в роли документации к коду. Но важно понимать, что модульные тесты не являются полной заменой для UI-тестов, а скорее служат дополнением, позволяющим уменьшить количество проверок в UI-тестах и их общего количества.

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

Зачем же нужны тесты?

Хорошие тесты обеспечивают стабильный рост программного продукта.

Хорошие тесты способствуют повышению качества приложения и качества архитектуры.

Хорошие тесты создают safety net и помогают найти баги как на этапе разработки, так и при последующем внесении изменений. Safety net — это образная сеть, которая помогает отлавливать баги, которые оказываются в приложении во время разработки.

Хорошие тесты должны выступать в роли документации. При чтении тестов должно становиться понятно, что делает тестируемый код.

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

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

Основные свойства хороших тестов

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

Хорошие тесты должны максимально защищать от багов при минимальных затратах на сопровождение.

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

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

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

Тесты должны быть повторяемыми. Они должны давать один и тот же результат при любом количестве запусков.

Антипаттерны

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

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

Пример дублирования кода тестов

Копирование логики из основного кода проекта для проверки этой же логики в тестах.
Такие проверки по факту ничего не проверяют, усложняют чтение теста и приводят к хрупким тестам. Хрупкий тест — это тест, который перестаёт работать после изменений внутренней структуры кода, но с сохранением алгоритма и интерфейса. Получаемые от тестируемой системы данные всегда должны сравниваться с конкретными значениями, объявленными в тестах, а не высчитываться с помощью алгоритма, особенно скопированного из клиентского кода. Это относится и к строкам: лучше использовать строки, объявленные в тестах.

Пример копирования логики для проверки значений

Условные операторы и циклы в коде тестов.
В тестах не должны использоваться условные операторы и циклы. Эти конструкции усложняют чтение тестов. В момент чтения кода тестов с условными операторами непонятно, какая часть теста будет выполнена. Обычно тест с ветвлением логики можно разделить на два разных теста. Циклы тоже должны быть развёрнуты в линейный код для повышения читаемости.

Пример использования условных операторов

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

Читайте также:  Iphone 11 pro max 256 комплектация

Пример тестирования приватных методов

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

Код, сложный для тестирования.
Как правило, это такой код, который содержит много логики и много зависимостей одновременно. Достаточное тестирование такого кода занимает много времени, а поддержка чрезвычайно сложна. Желательно отрефакторить такой код и разделить его на части, которые содержат сложную логику и не имеют зависимостей, и части, которые имеют много зависимостей и не имеют сложной логики. Тогда на первую часть можно будет написать Unit-тесты, а на вторую – модульные.

Примером тут можно считать наследников UIViewController при использовании паттерна MVC. У него много зависимостей, и он содержит всю логику. Эта проблема решается переходом на более подходящую архитектуру.

Модуль покрыт только одним видом тестов.
Если модуль покрыт только UI-тестами, то это значит, что эти UI-тесты являются слишком сложными и проверяют то, что можно проверить и модульными тестами. Если есть только модульные тесты, то это значит, что нет никаких проверок работоспособности UI и роутинга, а это тоже плохо. Тесты, которые могут быть перенесены из UI-тестов в модульные, должны быть туда перенесены.

Метрики тестирования

Code coverage довольно часто рассматривается как основная метрика качества тестов. Но у такого подхода есть несколько проблем и лучше не делать процент покрытия кода тестами самоцелью.

Во-первых, code coverage будет считаться, даже если тест просто выполняет код, но ничего не проверяет. Во-вторых, эта метрика никак не учитывает код, скрытый в сторонних и системных библиотеках. В-третьих, существует такой код, который очень тяжело протестировать (hard to test code). Попытка достичь высокого процента coverage будет стоить в таком случае слишком дорого. Будет более целесообразно сначала инвестировать время в рефакторинг кода и извлечение плохо тестируемого кода из модулей, а уже после этого покрыть функционал тестами.

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

Установка обязательного высокого code-coverage может начать подменять цели тестирования. Разработчики должны фокусироваться на качестве тестов критических частей системы, а в итоге будут искать способы достичь высокого процента покрытия всей системы.

К сожалению, не существует идеальной метрики тестирования, но есть пара рекомендаций. Лучше инвестировать своё время в тестирование поведения модулей, чем в тестирование наибольшего количества строк кода. Также следует уделять больше времени качественному тестированию сложной и важной для проекта логики, чем покрытию тестами очевидного и линейного кода.

Какие бывают тесты и их назначение

Тесты в мобильных приложениях делятся на три типа: Unit-тесты, модульные тесты и UI-тесты. У каждого типа тестов есть своё ограниченное применение.

Unit-тесты

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

Если нужно протестировать класс, который содержит в себе и сложную логику, и множество зависимостей, то такой класс должен быть отрефакторен, сложная логика выделена в отдельный класс, и уже на новый класс должны быть написаны Unit-тесты.

Unit-тесты желательно писать на системные модули (техническая аналитика, диплинки и другие модули, скрытые от конечных пользователей). Соответственно, стоит проектировать эти модули с учётом написания Unit-тестов и сокращать их зависимости до минимума.

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

Сложная логика — общее понятие, описывающее код, который содержит большое количество изменяемого стейта и/или большое количество ветвлений. Внесение изменений в такой код обычно сопряжено с риском его сломать.

Модульные тесты

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

Как правило, модульный тест для VIPER модуля пишется на связку классов, находящихся в иерархии модуля выше слоя view — например, на связку presenter-interactor-service, а view заменяется на мок ввиду сложности его тестирования. В других архитектурах будет схожая история: модульные тесты будет удобно писать, если из программного модуля будет исключён view-слой, а слой, содержащий логику, будет вызываться напрямую так же, как их вызывает view.

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

Для iOS-разработки это обычно связано с жизненным циклом view controler. Это значит, что если в коде приложения во view controler в методе viewDidLoad() вызывается метод start() у presenter, чтобы перевести presenter в активное состояние, то в тестах тоже должен быть вызван метод start() у presenter перед любыми другими действиями — например, нажатием кнопок на экране или запроса на дозагрузку данных. Такой подход позволять писать более надёжные тесты, которые максимально приближены к реальному использованию кода.

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

UI-тесты

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

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

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

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

Как правило, количество таких тестов должно быть меньше, чем модульных тестов, потому что для общего упрощения тестов бо́льшую часть проверок модуля можно перенести из UI-тестов в модульные без потери качества тестирования.

Пирамида тестирования

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

Таким образом, мы подходим к понятию «пирамида тестирования». В классическом виде пирамида тестирования — это действительно пирамида, в основании которой лежат Unit-тесты, в середине — модульные, а в вершине — UI- и end-to-end-тесты. Смысл в том, что тесты, которые лежат в пирамиде ближе к основанию, легки в написании и поддержке, но не дают достаточной степени защиты. А тесты, лежащие в пирамиде ближе к вершине, дают отличную защиту от багов, но сложны в написании и дают медленную обратную связь.

Читайте также:  Красивая девочка 12 лет с айфоном

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

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

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

О том, что можно заменять на моки

Unit-тесты

Желательно, чтобы у кода, который тестируют Unit-тесты, не было изменяемых зависимостей (исключение — зависимости от Date(), но об этом ниже). Если организовать класс так, что в нём не будет изменяемых зависимостей, то это сильно упростит тестирование данного класса. Это связано с тем, что возвращаемые и проверяемые значения будут зависеть только от входных параметров. Такой подход позволяет протестировать логику максимально полно и не упустить никаких неожиданных сайд-эффектов.

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

* Изменяемые зависимости — это зависимости, которые возвращают разный результат при изменении своего состояния. Такие зависимости могут значительно усложнять тестирование кода.

* Неизменяемые зависимости — это зависимости, которые всегда возвращают одинаковые значения или никак не влияют на тестируемый код.

Пример кода, который удобно покрыть Unit-тестами

Модульные тесты

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

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

Например, UserDefaults можно не мокировать, а использовать реальный, передавая его как зависимость в конструктор тестируемого класса. В продакшн-коде использовать UserDefaults.shared , а в тестах создавать UserDefaults через UserDefaults(suiteName: #file) и очищать его от старых данных. То же относится и к встроенным базам данных, но они могут требовать более сложной дополнительной подготовки теста.

В модульных тестах обычно заменяется на моки view-слой, слой роутинга (если у него есть зависимость от UIViewController) и network client. Смысл в том, чтобы замокировать зависимости от UIKit и прочих, которые сложно тестировать.

В итоге модульные тесты обычно покрывают связку presenter-interactor-service (из архитектуры VIPER) и все их внутренние классы-хелперы.

Пример кода, который удобно покрыть модульными тестами

UI-тесты

У нас в UI-тестах используются моки для сервиса звонков, чтобы можно было проверить поведение приложения после звонка. Ещё мокаем сервис проведения платежей, чтобы проверить корректную работу UI после платежа.

В самом же коде UI-тестов подменяются ответы от сервера (если это не end-to-end UI-тест), выставляются нужные значения UserDefaults, значения фича тоглов и AB-тестов.

Про работу с Date

Для получения текущей даты или для формирования какого-либо её строкового представления в коде часто используется вызов Date(). Такой код плохо поддаётся тестированию, т.к. результат работы таких функций будет изменяться с течением времени. Грубо говоря, функция получения текущего времени является неявной системной зависимостью, а для улучшения тестируемости эта зависимость должна стать явной.

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

Пример реализации DateProvider

Анатомия хорошего теста

Важно понимать, что тесты напрямую связаны с клиентским кодом и его архитектурой, и, скорее всего, не получится просто взять и написать тест на модуль, который не был спроектирован с учётом покрытия его тестами. Это является одной из основных причин провалов при внедрении Unit и модульного тестирования в проекты.

Чтобы класс или связку классов можно было протестировать, они не должны содержать в себе hard-to-test (нетестируемых или сложных для тестирования) зависимостей внутри себя.

Например, CLLocationManager является hard-to-test зависимостью, потому что его результаты зависят от положения устройства в пространстве. Зависимости такого вида должны быть инкапсулированы в отдельных классах, закрыты протоколом и передаваться в модули явно, чтобы в тестах такие зависимости можно было заменить на заглушки.

Другим примером hard-to-test-code является UI-слой приложения, в рамках модульных тестов он не тестируется. Вся логика должна быть вынесена из UI-слоя в презентер, чтобы её можно было протестировать.

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

Arrange-Act-Assert

Для структурирования кода теста стоит использовать паттерн Arrange-Act-Assert. Если все тесты будут написаны в одном стиле с использованием этого паттерна, то разработчикам будет легче ориентироваться в коде тестов и поддерживать их.

Сначала в верхней части функции теста идёт секция подготовки условий для теста, она называется Arrange. В ней создаётся тестируемый модуль, подменяются запросы к серверу, модуль приводится к нужному состоянию и т. д. Эта секция может занимать несколько строчек, а повторяющиеся между тестами строчки подготовки могут быть вынесены в функцию setUp() или в отдельные функции-хелперы.

Дальше идёт секция Act, в ней выполняется действие, результат которого мы хотим протестировать. Как правило, эта часть занимает одну строчку. Если для выполнения какого-то одного действия требуется вызов нескольких функций, то, возможно, произошла ошибка в проектировании клиентского кода.

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

Для большего повышения читаемости блоки Arrange-Act-Assert можно обозначить комментариями или разделить пустыми строками, чтобы после одного взгляда на тест становилось понятно, где какая проверка.

Вместо заключения

На этом пока всё. При разработке кода и при проектировании архитектуры задавайте себе вопрос: «Насколько просто будет тестировать такой код?», «Нет ли в нём hard-to-test зависимостей?», «Удобно ли будет пользоваться интерфейсом модуля при его тестировании?» К сожалению, если модуль удобно тестировать, то это не всегда значит, что у него хорошая архитектура, но если модуль протестировать нельзя, то это однозначно говорит о низком качестве проектирования.

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

Довольно сложно покрыть все аспекты качественного тестирования кода в одной небольшой статье. Но если вас зацепила эта тема и захотелось поглубже в неё погрузиться, то советую прочитать книгу «Принципы юнит-тестирования», автор Хориков Владимир. Бо́льшая часть этой статьи написана под вдохновением от прочтения этой книги, так что однозначно рекомендую к прочтению.

Продолжение этой серии, посвящённое практике тестирования, будет в следующей статье, в ней мы подробно рассмотрим, как пользоваться фреймворком для тестирования Quick, как лучше организовать тесты, как их упростить и как повысить их качество. А статью о UI-тестировании в iOS уже можно прочитать тут.
Stay tuned!

Источник

Оцените статью