Перейти к содержанию

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

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

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

Тестирование — это цикл исправлений, тестирования и либо передачи в релиз, либо возврата в тестирование.

Почему тест

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

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

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

И последнее, но не менее важное: более автоматизированное тестирование означает меньше времени, потраченного на ручное QA, высвобождая драгоценное время.

Статический анализ

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

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

React Native поставляется с двумя такими инструментами, настроенными из коробки: ESLint для линтинга и TypeScript для проверки типов.

Написание тестируемого кода

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

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

Чтобы сделать ваше приложение более тестируемым, начните с отделения части представления приложения — ваших компонентов React — от бизнес-логики и состояния приложения (независимо от того, используете ли вы Redux, MobX или другие решения). Таким образом, вы сможете поддерживать тестирование бизнес-логики, которая не должна зависеть от компонентов React, независимо от самих компонентов, чья работа заключается в основном в рендеринге пользовательского интерфейса вашего приложения!

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

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

Пишем тесты

После написания тестируемого кода пришло время написать несколько реальных тестов! Шаблон React Native по умолчанию поставляется с фреймворком тестирования Jest. Он включает в себя предустановку, адаптированную к данной среде, так что вы можете начать продуктивную работу без изменения конфигурации и моков. Вы можете использовать Jest для написания всех типов тестов, описанных в этом руководстве.

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

Структурирование тестов

Ваши тесты должны быть короткими и в идеале проверять только одну вещь. Давайте начнем с примера модульного теста, написанного с помощью Jest:

1
2
3
it('given a date in the past, colorForDueDate() returns red', () => {
    expect(colorForDueDate('2000-10-20')).toBe('red');
});

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

  1. Arrange — некоторое предварительное условие
  2. Act — некоторое действие, выполняемое тестируемой функцией.
  3. Assert — ожидаемый результат

Это также известно как AAA (Arrange, Act, Assert).

Jest предлагает функцию describe, чтобы помочь структурировать ваши тесты. Используйте describe для объединения всех тестов, относящихся к одной функциональности. Описания могут быть вложенными, если вам это нужно. Другие функции, которые вы часто будете использовать, это beforeEach или beforeAll, которые вы можете использовать для настройки тестируемых объектов. Подробнее читайте в Jest api reference.

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

Наконец, как разработчики мы любим, когда наш код работает отлично и не дает сбоев. С тестами часто бывает наоборот. Думайте о неудачном тесте как о хорошей вещи! Когда тест не работает, это часто означает, что что-то не так. Это дает вам возможность устранить проблему до того, как она повлияет на пользователей.

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

Юнит-тесты охватывают самые маленькие части кода, например, отдельные функции или классы.

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

Самое замечательное в модульных тестах то, что они быстро пишутся и выполняются. Поэтому в процессе работы вы получаете быструю обратную связь о том, прошли ли ваши тесты. В Jest даже есть возможность постоянно запускать тесты, связанные с редактируемым кодом: Watch mode.

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

Моки

Иногда, когда ваши тестируемые объекты имеют внешние зависимости, вы захотите их "замокать". "Моки" — это когда вы заменяете некоторую зависимость вашего кода своей собственной реализацией.

Вообще, использование реальных объектов в тестах лучше, чем использование макетов, но бывают ситуации, когда это невозможно. Например, когда ваш JS-юнит-тест зависит от нативного модуля, написанного на Java или Objective-C.

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

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

Поэтому вы можете предоставить имитационную реализацию сервиса, фактически заменив тысячи строк кода и несколько подключенных к Интернету термометров!

Jest поставляется с поддержкой мокинга от уровня функций до мокинга на уровне модулей.

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

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

При интеграционном тестировании реальные отдельные блоки объединяются (как в вашем приложении) и тестируются вместе, чтобы убедиться, что их взаимодействие работает так, как ожидается. Это не значит, что здесь не используется мокинг: мокинг все равно понадобится (например, для имитации связи с погодным сервисом), но его понадобится гораздо меньше, чем при модульном тестировании.

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

  • объединяет несколько модулей вашего приложения, как описано выше
  • использует внешнюю систему
  • выполняет сетевой вызов к другому приложению (например, API погодного сервиса)
  • выполняет любой вид ввода-вывода файлов или баз данных

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

Тесты компонентов

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

Для тестирования компонентов React есть две вещи, которые вы можете захотеть протестировать:

  • Взаимодействие: для обеспечения правильного поведения компонента при взаимодействии с пользователем (например, при нажатии кнопки).
  • Рендеринг: обеспечение правильного вывода рендеринга компонента, используемого React (например, внешний вид и расположение кнопки в пользовательском интерфейсе).

Например, если у вас есть кнопка, у которой есть слушатель onPress, вы хотите проверить, что кнопка правильно отображается и что нажатие кнопки правильно обрабатывается компонентом.

Существует несколько библиотек, которые могут помочь вам протестировать их:

  • Test Renderer, разработанный вместе с ядром React, предоставляет рендерер React, который можно использовать для рендеринга компонентов React в чистые объекты JavaScript, без зависимости от DOM или нативного мобильного окружения.
  • React Native Testing Library построена поверх тестового рендерера React и добавляет API fireEvent и query, описанные в следующем параграфе.

Тесты компонентов — это только тесты JavaScript, выполняемые в среде Node.js. Они не учитывают код iOS, Android или другой платформы, который лежит в основе компонентов React Native. Из этого следует, что они не могут дать вам 100% уверенности в том, что все работает для пользователя. Если в коде iOS или Android есть ошибка, они ее не найдут.

Тесты компонентов

Тестирование взаимодействия с пользователем

Помимо отображения пользовательского интерфейса, ваши компоненты обрабатывают такие события, как onChangeText для TextInput или onPress для Button. Они также могут содержать другие функции и обратные вызовы событий. Рассмотрим следующий пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function GroceryShoppingList() {
    const [groceryItem, setGroceryItem] = useState('');
    const [items, setItems] = useState<string[]>([]);

    const addNewItemToShoppingList = useCallback(() => {
        setItems([groceryItem, ...items]);
        setGroceryItem('');
    }, [groceryItem, items]);

    return (
        <>
            <TextInput
                value={groceryItem}
                placeholder="Enter grocery item"
                onChangeText={(text) =>
                    setGroceryItem(text)
                }
            />
            <Button
                title="Add the item to list"
                onPress={addNewItemToShoppingList}
            />
            {items.map((item) => (
                <Text key={item}>{item}</Text>
            ))}
        </>
    );
}

При тестировании взаимодействия с пользователем тестируйте компонент с точки зрения пользователя — что находится на странице? Что меняется при взаимодействии?

Как правило, лучше использовать то, что пользователи могут видеть или слышать:

И наоборот, вам следует избегать:

  • создание утверждений на пропсах или состоянии компонентов
  • запросы идентификаторов тестов

Избегайте тестирования деталей реализации, таких как пропсы или состояние — хотя такие тесты работают, они не ориентированы на то, как пользователи будут взаимодействовать с компонентом, и склонны ломаться при рефакторинге (например, когда вы хотите переименовать некоторые вещи или переписать класс компонента с использованием хуков).

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

Библиотеки тестирования компонентов, такие как React Native Testing Library, облегчают написание тестов, ориентированных на пользователя, благодаря тщательному выбору предоставляемых API. В следующем примере используются методы fireEvent changeText и press, которые имитируют взаимодействие пользователя с компонентом, и функция запроса getAllByText, которая находит совпадающие узлы Text в выводимых данных.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
test('given empty GroceryShoppingList, user can add an item to it', () => {
    const {
        getByPlaceholderText,
        getByText,
        getAllByText,
    } = render(<GroceryShoppingList />);

    fireEvent.changeText(
        getByPlaceholderText('Enter grocery item'),
        'banana'
    );
    fireEvent.press(getByText('Add the item to list'));

    const bananaElements = getAllByText('banana');
    expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});

Этот пример не проверяет, как изменяется состояние при вызове функции. Он тестирует, что происходит, когда пользователь изменяет текст в TextInput и нажимает Button!

Тестирование рендеринга

Snapshot testing — это продвинутый вид тестирования, поддерживаемый Jest. Это очень мощный и низкоуровневый инструмент, поэтому при его использовании рекомендуется повышенное внимание.

"Снимок компонента" — это JSX-подобная строка, созданная пользовательским сериализатором React, встроенным в Jest. Этот сериализатор позволяет Jest переводить деревья компонентов React в строку, удобную для чтения человеком. Другими словами: снимок компонента — это текстовое представление вывода рендеринга вашего компонента, сгенерированное во время выполнения теста. Он может выглядеть следующим образом:

1
2
3
4
5
6
7
8
9
<Text
  style={
    Object {
      "fontSize": 20,
      "textAlign": "center",
    }
  }>
  Welcome to React Native!
</Text>

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

У моментальных снимков есть несколько слабых мест:

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

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

Мы рекомендуем использовать только маленькие снимки (см. правило no-large-snapshots). Если вы хотите протестировать изменение между двумя состояниями компонентов React, используйте snapshot-diff. Если вы сомневаетесь, предпочитайте явные ожидания, как описано в предыдущем параграфе.

Тестирование рендеринга

E2E тесты

В сквозных тестах (E2E) вы проверяете, что ваше приложение работает так, как ожидается, на устройстве (или симуляторе/эмуляторе) с точки зрения пользователя.

Это делается путем создания вашего приложения в конфигурации релиза и запуска тестов на нем. В тестах E2E вы больше не думаете о компонентах React, API React Native, хранилищах Redux или любой бизнес-логике. Это не является целью тестов E2E, и они даже недоступны для вас во время тестирования E2E.

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

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

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

Постарайтесь охватить E2E-тестами жизненно важные части вашего приложения: поток аутентификации, основные функции, платежи и т.д. Используйте более быстрые JS-тесты для не жизненно важных частей вашего приложения. Чем больше тестов вы добавляете, тем выше ваша уверенность, но также и тем больше времени вы будете тратить на их поддержку и выполнение. Подумайте о компромиссах и решите, что лучше для вас.

Существует несколько инструментов E2E тестирования: в сообществе React Native популярным фреймворком является Detox, поскольку он адаптирован для приложений React Native. Другой популярной библиотекой в области приложений для iOS и Android является Appium или Maestro.

E2E тесты

Резюме

Мы надеемся, что вы получили удовольствие от чтения и узнали что-то новое из этого руководства. Существует множество способов тестирования приложений. Поначалу может быть трудно решить, какой из них использовать. Однако мы уверены, что все станет понятно, как только вы начнете добавлять тесты в свое потрясающее приложение React Native. Так чего же вы ждете? Начните тестирование!

Ссылки

Данное руководство изначально было написано и полностью предоставлено Войтехом Новаком.

Комментарии