Тестирование в Android. Часть 1: введение

Открываем цикл материалов по тестированию Android-приложений
12 минут2734

Этой статьёй мы открываем целый цикл материалов, посвящённых тестированию Android-приложений. Мы рассмотрим все варианты тестирования, которые позволят нам полностью покрыть тестами практически любое приложение. Просьба пристегнуть ремни — мы стартуем!

Какие тесты бывают: обзор инструментов

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

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

Мы можем тестировать разные части приложения, например, бизнес-логику или визуальный интерфейс, или запросы в интернет (базу данных). Давайте посмотрим на тесты, наиболее характерные для платформы Android: локальные и инструментальные.

Локальные тесты

Локальные тесты запускаются на локальной JVM (Java Virtual Machine). Это значит, что такие тесты могут запускаться на разных устройствах — локальная JVM везде одинаковая. Такие тесты в основном используются для тестирования бизнес-логики, то есть не задействуют пользовательский интерфейс или непосредственно компоненты Android: тестируется код, независимый от платформы. 

Если нужно тестировать классы, завязанные на Android (Activity/Fragments, ViewModels, Services и т. п.), на помощь приходит Robolectric. Но об этом мы поговорим в следующей статье.

Инструментальные тесты

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

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

Фреймворки

Для тестирования UI существуют различные решения. Мы с вами будем пользоваться только нативными, такими как Espresso, но стоит немного рассказать и про кроссплатформенные. Некоторые платформы (типа Appium, Selenium) позволяют тестировать приложения, созданные не только для Android, но и для iOS или десктопа. Это очень удобно, когда одно и то же приложение разрабатывается для разных платформ или операционных систем: разработчикам каждого приложения не приходится писать тесты самостоятельно — тесты пишутся один раз, централизованно, и все приложения тестируются на единой платформе. Но это касается только тестирования интерфейса и возможно только в тех случаях, когда приложения выглядят одинаково на разных платформах. Если экраны сильно различаются или приложения совсем не похожи по внешнему виду и функционалу, то одинаковые тесты написать не получится. Тем не менее, несмотря на такие ограничения, это довольно популярный подход для кроссплатформенного тестирования.

Основные принципы тестирования

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

Пишите код и тестируйте его итеративно

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

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

Тут мы в первую очередь говорим о unit-тестах. Что значит unit? Это условное название для блока кода. Это может быть метод, часть метода, класс. Соответственно unit-тестирование — это тестирование отдельных блоков кода. Для каждого блока написанного кода вы пишете свой unit-тест. Ваши тесты должны покрывать все возможности использования вашего блока кода, стандартное и нестандартное поведение. Допустим вы должны написать тест для метода:

fun validatePhoneNumber(number : Int) : Boolean {...}

Вы должны будете написать тест для этого метода и при этом проверить максимально возможные варианты, передать в качестве аргумента Int, Long, отрицательное значение, String и т. д. 

Пример итеративной разработки цикличен и приведен на схеме ниже:

The testing development cycle consists of writing a failing unit           test, writing code to make it pass, and then refactoring. The entire           feature development cycle exists inside one step of a larger,           UI-based cycle.

Разбивайте ваше приложение на слои

Чтобы тестирование проходило легче, старайтесь разбивать весь ваш код так, чтобы каждый блок кода отвечал за конкретное действие (вспоминаем принципы SOLID). Как минимум имеет смысл разбить приложение на три слоя: UI, бизнес-логика и данные.

Настройте свое тестовое окружение

Обратите внимание, что, помимо основной папки с названием вашего проекта, у вас всегда создаются две одноимённые папки зеленого цвета:

  • папка androidTest предназначена для инструментальных тестов;
  • папка test — для локальных unit-тестов.

Выберите устройства для тестов 

Это правило касается в основном инструментальных тестов.

Также имейте в виду, что вы можете запускать инструментальные тесты на разных типах устройств:

  • реальные устройства;
  • эмуляторы (эмулятор в Студии или Genymotion);
  • симуляторы устройств, например, Robolectric (об этом позже).

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

Старайтесь использовать в тестах реальные объекты

При написании тестов и тестировании определённых классов/функций вам может потребоваться создание экземпляров необходимых для этого классов. Например, вы передаёте в тестируемый метод класс Person, и вам нужно решить, создаёте ли вы этот класс самостоятельно или используете так называемый мок (mock — заглушка с пустыми полями и методами, которые ничего не возвращают). В целом старайтесь создавать реальные объекты, а не моки, особенно если они удовлетворяют следующим требованиям:

  • это data-классы или POJO;
  • от этих объектов зависит корректное выполнение теста.

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

  • Длительные операции типа обработки больших файлов: лучше использовать моки таких файлов, просто чтобы не тратить время каждый раз.
  • Объекты, которые сложно создать в изолированном окружении теста, например Fragment или Activity.

На будущее

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

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

Все тесты можно поделить на

  • маленькие: unit-тесты; обычно пишутся в большом количестве и покрывают бизнес-логику приложения;
  • средние: интеграционные тесты, проверяющие взаимодействие между слоями и модулями приложения; пишутся в небольшом количестве;
  • большие: end-to-end тесты, проверяющие навигацию, user-flow и общую работу приложения; этим типом тестирования разработчики занимаются меньше всего; часто, когда нужна помощь команде QA.

A pyramid containing three layers

Чем выше тесты находятся в этой пирамиде, тем тяжелее их писать и сложнее поддерживать. Рекомендуемое соотношение тестов в пирамиде: 70% unit-тестов, 20% интеграционных и 10% остальных.

Unit-тесты

Это тесты, которые должны проверять всю работу классов и методов класса в максимальном количестве случаев: нормальная работа, работа с ошибками, с нестандартными данными и т. п. Когда вы добавляете новые методы в класс или изменяете существующие — обязательно пишите к ним новые тесты и обновляйте уже написанные. Если тесты используют какие-то части фреймворка Android, для их написания пользуйтесь помощью androidx.test.

Robolectric

Эта библиотека используется для тестирования кода, завязанного на платформу Android (компоненты Android, ресурсы и т. д.) и запускает их на внутренней эмуляции устройства. Вам даже не нужно запускать приложение на эмуляторе или реальном устройстве, чтобы прогнать тесты, что очень удобно и экономит много времени. Поддерживаются тесты для API 16 и выше. Robolectric позволяет тестировать код платформы Android, касающийся

  • компонентов с жизненным циклом;
  • ресурсов приложения.

Инструментальные тесты

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

Интеграционные тесты (средние тесты)

Помимо тестирования отдельных классов и блоков кода (unit-тесты, Robolectric, инструментальные тесты) можно тестировать приложение на уровне модулей, проверяя взаимодействие отдельных компонентов или целых модулей между собой. Можно проверять

  1. работу целого Фрагмента (Fragment);
  2. работу на уровне вашей БД;
  3. переходы между разными Activity, навигацию;
  4. работу UI нескольких фрагментов на экране (здесь, в отличие от вышеописанных тестов, вам может понадобиться запуск на реальном устройстве).

Espresso

    Используйте эту библиотеку, если вам нужно прогонять инструментальные тесты, завязанные на UI и компоненты Android:

  • Проверка UI конкретного экрана: нажатие на кнопки, использование EditText и других View;
  • Проверка работы элементов в RecyclerView;
  • Проверка запуска и корректной работы Intents;
  • Тестирование WebView.

Большие тесты

Тесты, которые, грубо говоря, тестируют ваше приложение полностью. Имеет смысл использовать их, когда ваше приложение полностью готово или имеет рабочий прототип без каких-то явных проблем. Такие тесты просто проходят по всему приложению, запуская все экраны подряд, проверяя навигацию в приложении, открытие и закрытие экранов, user flow. Эти тесты могут проводиться как в автоматическом режиме, так и вручную с помощью мануального тестирования. Называются такие тесты Smoke test, по аналогии с замкнутым пространством без щелей: если откуда-то идет дым, значит там явно есть щель, которую нужно закрыть.

Такие тесты запускаются на устройстве или эмуляторе, чтобы убедиться, что приложение просто работает и все экраны открываются, переход между модулями и экранами осуществляется нормально, нет никаких узких мест в навигации приложения. Возникает закономерный вопрос: «Почему разработчик не может сам открыть приложение и все его экраны, особенно если их там всего 5?» Ответ очень прост: фрагментация. Вряд ли у разработчика под рукой находится хотя бы топ-10 самых популярных смартфонов на рынке, не говоря уже про остальные топ-100 популярных моделей. Да и вряд ли разработчик будет каждый раз открывать все экраны на каждом из 10 устройств, чтобы убедиться, что всё работает как надо. Для этого есть внешние облачные сервисы и фермы устройств, такие как Firebase Test Lab, которые запустят и погоняют ваше приложение на десятках и сотнях разных устройств с разными экранами, разрешениями, памятью, версией ОС и т. п.

Что такое TDD

Аббревиатура расшифровывается как Test Driven Development — разработка по принципам тестирования. Это значит, что вы сначала пишете тесты, а только потом — необходимый код, который нужен, чтобы эти тесты пройти. Таким образом вы добиваетесь очень надежного и тестируемого кода. Вы пишете:

  • код со 100% покрытием (по крайней мере с высоким процентом покрытия);
  • код с добавлением функционального подхода;
  • классы с использованием dependency injection, потому что так эти классы проще тестировать.

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

  • Тест: это блок кода, который описывает тестируемое состояние и результат этого тестирования. Например, метод addOne() : Int добавляет 1 к числу и возвращает это число. Также этот блок кода содержит проверочную часть, которая подтверждает корректность проверки. Проверочная часть может содержать одно или несколько утверждений (Assertions).
  • Утверждение (Assertion): функция, которая проверяет результат выполнения блока кода на соответствие нашим ожиданиям. Эта функция может проверять разные утверждения. К примеру, что наш метод addOne(2):
    • вернет 3;
    • вернет Int;
    • выполнится только один раз и т. д.

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

Тестируйте только то, что нужно тестировать

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

Конечно, это не единственный подход к тестированию. Есть, как минимум, два подхода, которые принципиально различаются: описанный выше и противоположный ему. Сторонники противоположного подхода считают, что надо покрыть тестами 100% кода. Некоторые разработчики ратуют за качество тестов. Истина, как обычно, где-то посередине и зависит от условий разработки и требований к проекту.

Пишите один тест и много утверждений

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

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

Пишите тесты до кода

Это очень редкая практика среди программистов, особенно на коммерческих проектах, где заказчик не готов платить на 30–50% больше за разработку приложения, но в небольших или личных проектах это окупается, потому что, составляя тесты для будущего кода, вы до написания реального кода понимаете, где могут быть ошибки, где есть слабые места и как их избежать. Это позволяет вам сразу писать код, который будет более продуманным и надёжным, экономя вам время в будущем.

Автоматизируйте тесты

При TDD тестов появляется довольно много, и прогон их всех может продолжаться достаточно долго. Используйте соответствующие плагины или скрипты для автоматического запуска тестов. Их можно настроить или в Android Studio, или в GitHub (или иной системе контроля версий) и запускать их автоматически перед тем, как вливать ваш код в репозиторий. Ваш код просто не попадёт в репозиторий, если какие-то тесты провалятся.

В следующей статье мы напишем свои первые тесты и на практике освоим unit-тестирование. Не переключайтесь!

Читайте больше полезных статей для начинающих Android-разработчиков:

А если затянет — приходите на факультет Android-разработки. Во время учебы вы разработаете Android-приложение и выложите его в Google Play, даже если никогда не программировали. А также освоите языки Java и Kotlin, командную разработку, Material Design и принципы тестирования.

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