Одной из самых сложных задач при написании надежных, логичных, поддерживаемых и легко читаемых тестов является их правильное структурирование. Несоблюдение определенной структуры в каждом модульном тесте может привести к нестабильности, то есть ненадежности тестов — кошмару разработчика.
Существует множество подходов к модульному тестированию, среди которых наиболее популярной практикой является шаблон, или паттерн, Arrange-Act-Assert. Настройка — Выполнение — Проверка, или Три А.
Этот шаблон (паттерн) предполагает разделение (структурирование) юнит-тестов на три стандартных этапа:
- Arrange (Настройка): Создать тестовое окружение.
- Act (Выполнение): Выполнить тестовый код.
- Assert (Проверка): Проверка результатов.
В этом руководстве вы узнаете, что такое паттерн AAA, как он работает, какие преимущества дает и какова его роль в автоматизации юнит-тестирования.
AAA в модульном тестировании
Паттерн Arrange-Act-Assert (чаще называемый AAA или Три A), является широко известным подходом к структурированию тестов. Он был первоначально предложен Биллом Уэйком в 2001 г., а затем упомянут в известной книге Кента Бека «Test Driven Development на примерах» в 2002 г.
Читайте также: TDD или Разработка через тестирование. Гайд по Test-Driven Development
Итак, паттерн AAA рекомендует структурировать тесты на три отдельные фазы:
- Организовать (Arrange) или подготовить все необходимое для выполнения теста.
- Воздействовать (Act) на тестируемый код, выполняя его.
- Проверить (Assert) полученные результаты, сопоставив с ожидаемыми.
AAA-паттерн улучшает читаемость и сопровождаемость тестов, напоминая шаблонную структуру Given-When-Then, разработанную Даниэлем Терхорстом-Нортом и Крисом Мэттсом в рамках BDD (Behavior-Driven Development). Сегодня почти все современные инструменты юнит-тестирования с синтаксисом BDD поощряют использование AAA-паттерна. Пора рассмотреть подробнее три фазы, из которых он состоит.
Подготовка (Arrange)
На этапе Подготовки вы подготавливаете все необходимое для выполнения теста, чтобы убедиться, что он даст точные результаты. Подготовка подразумевает инициализацию объектов, настройку зависимостей и создание тестового окружения.
Примеры операций, которые необходимо выполнить на этом этапе:
- Создание экземпляров тестируемых классов (инстанцирование)
- Инициализация глобальных или локальных переменных с нужными значениями
- Создание объектов-макетов (моков) для имитации внешних сервисов
- Наполнение базы данных тестовыми данными
- Настройка нужных параметров и конфигураций
Тщательно организуя контекст и состояния теста, вы гарантируете, что последующие действия и тестовые утверждения (ассерты) будут происходить в контролируемом и предсказуемом окружении. Такой подход повышает надежность тестов и снижает вероятность flaky-поведения.
Действие (Act)
На этапе действия вы выполняете конкретную функцию, которую хотите протестировать. Этот этап включает в себя взаимодействие с SUT (тестируемой системой) путем вызова метода, или выполнения функции, которую вы подготовили на предыдущем этапе.
В большинстве модульных тестов этот этап соответствует вызову функции или метода объекта/класса для выполнения определенной операции. Цель состоит в том, чтобы выполнить действие, которое приведет к результату, который вы собираетесь проверить на следующем этапе.
Чтобы тест было легко читать и обслуживать, это действие должно быть кратким и целенаправленным. Как правило, достаточно одной строки кода, в которой вызывается тестируемый метод или функция. Простота облегчает наблюдение и понимание влияния тестируемого кода.
Проверка (Assert)
На этом этапе проверяется соответствие результатов модульного теста вашим ожиданиям. Этот этап подразумевает проверку результатов, полученных на этапе Act, на соответствие ожидаемым значениям, чтобы подтвердить, что SUT ведет себя так, как нужно.
Обычно для этого используются методы так называемых утверждений (ассертов), сравнивающие фактические результаты с ожидаемыми. Например:
- Проверка того, что метод возвращает правильное значение
- Проверка того, что состояние объекта изменилось в соответствии с ожиданиями
- Проверка того, что выбрасывается ошибка-исключение при определенных условиях
Чтобы сохранить читабельность и эффективность теста, шаг Assert должен быть четким и конкретным. Именно поэтому рекомендуется использовать одно утверждение — или небольшой набор связанных утверждений — в каждом модульном тесте.
Пример юнит-теста, написанного с использованием паттерна AAA
Чтобы лучше понять, как работает паттерн Arrange-Act-Assert, посмотрим на приведенный ниже пример юнит-теста Mocha, структурированный в соответствии с AAA-паттерном:
import { expect } from "chai" import { MathUtils } from "src/utils/MathUtils"; describe("MathUtils Tests", function () { describe("#getFibonacciNumber()", function () { it("should return the correct Fibonacci number", function () { // Arrange: Initialize the class to test const mathUtils = new MathUtils() // Act: Test the method of a class const result = mathUtils.getFibonacciNumber(6) // Assert: Verify that the method produces the expected outcome expect(result).to.equal(8) }) }) })
Теперь разберем этот код, чтобы выделить строки, соответствующие каждой из трех фаз.
(Более сложные примеры — посмотрите этот и этот тесты Mocha на GitHub.)
Arrange
const mathUtils = new MathUtils()
Здесь вы настраиваете тест, создавая экземпляр класса MathUtils
.
В этом примере этап Arrange происходит непосредственно в функции it()
. Однако если тест требует более сложной настройки, например базы данных, этот этап часто выполняется в хуках beforeAll()
или beforeEach()
.
Обратите внимание, что импорт и функции describe()
не считаются частью этапа Arrange, который обычно включает только первые строки функций it()
и/или код в хуках beforeAll()
и beforeEach()
.
Act
const result = mathUtils.getFibonacciNumber(6)
В этом шаге вы используете объект mathUtility
, инициализированный ранее, для выполнения метода getFibonacciNumber()
с заданными входными данными.
Примечание: В этом примере входные данные простые и могут быть переданы непосредственно в тестируемую функцию. При работе с более сложными или множественными входными данными следует назначить их специальным переменным на шаге Arrange, а затем использовать эти переменные на этапе Act.
Assert
expect(result).to.equal(8)
Наконец, вы используете метод expect()
из Chai, чтобы проверить, что результат метода getFibonacciNumber()
совпадает с ожидаемым результатом.
Примечание: В данном случае результат простой и может быть проверен напрямую. Для более сложных выводов следует назначить их специальным переменным на шаге Arrange, а затем использовать их на шаге Assert.
Причины использовать ААА
Ознакомьтесь с основными преимуществами использования этого паттерна.
Независимость от языка и фреймворка тестирования
Как и другие паттерны проектирования, паттерн Arrange-Act-Assert не привязан к какому-либо языку программирования или фреймворку. Это означает, что его можно применять в любом фреймворке юнит-тестирования, включая Mocha и Jest на JavaScript, JUnit на Java и pytest на Python.
Такая последовательность позволяет команде разработчиков использовать единый подход к модульному тестированию в разных проектах, независимо от используемой технологии.
Упорядочивание кода
Паттерн способствует улучшению организации кода, побуждая вас разделять модульные тесты на три отдельные фазы. Такая фиксированная структура помогает поддерживать последовательный формат тестов, относящихся к разным участкам кода.
Выделяя этапы настройки-выполнения-проверки, паттерн уменьшает путаницу. Это гарантирует, что каждый тест будет сфокусирован и хорошо структурирован, что приведет к созданию более удобного тестового кода.
Упрощает понимание ваших модульных тестов коллегами и наоборот
Прямым следствием организованности в коде является улучшение читабельности и ясности. Юнит-тест, написанный с использованием Arrange-Act-Assert, по своей сути прост, поскольку он состоит из трех простых шагов.
Вы сможете легко понять чужие тесты, написанные с использованием этого паттерна, даже если вы незнакомы с конкретным инструментом тестирования или используемой технологией. Особенно, если выбранная технология тестирования имеет интуитивно понятный API ассертов.
Поощряет разработку, управляемую тестами
Шаблон модульного тестирования AAA поддерживает TDD (Test-Driven Development), создавая стандартную структуру ваших тестов. Каждый юнит-тест, соответствующий паттерну, должен состоять из трех шагов, каков бы ни был тестируемый код.
Таким образом, вы можете определить ожидаемое поведение кода еще до того, как код будет реализован. Это соответствует принципам TDD, когда сначала создаются тесты, а потом желаемая функциональность. Это позволяет направлять разработку таким образом, чтобы код соответствовал заданным требованиям.
Упрощает рефакторинг
Шаблон Arrange-Act-Assert поддерживает рефакторинг, изолируя изменения в определенных этапах теста, не затрагивая общую структуру теста.
Например, если вы измените способ приема входных данных методом класса, вам нужно будет обновить только шаг Act. Пока класс использует ту же логику для инстанцирования и метод выдает тот же результат, этапы Arrange и Assert могут оставаться неизменными.
Это позволяет уверенно рефакторить код, зная, что тесты потребуют минимальных обновлений.
Изолирует проблемы
Подход AAA к модульному тестированию способствует изоляции проблем, обеспечивая, чтобы каждая из трех фаз — Arrange, Act и Assert — оставалась сфокусированной на своей конкретной задаче. Такое четкое разделение делает тесты более модульными и безопасными, поскольку каждый этап отвечает за отдельную часть процесса тестирования. Разделяя настройку, выполнение и проверку, вы также уменьшаете дублирование тестовой логики.
Изучение лучших практик
С годами паттерн Arrange-Act-Assert стал стандартом де-факто. В настоящее время он упоминается и рекомендуется в нескольких руководствах по лучших практиках, включая:
- «JavaScript Testing Best Practices» на GitHub
- «Node.js Integration Test Best Practices» на GitHub
- «Unit Test Basics» в документации Visual Studio
- «How To Write a Test» guide в блоге Cypress
- Документация Pytest
Роль AAA в автоматизации
Структурированный подход паттерна Arrange-Act-Assert играет большую роль в эффективной автоматизации юнит-тестов. В частности, этап Assert облегчает понимание результатов тестирования при их выполнении в конвейерах CI/CD.
В паттерне AAA каждый тест заканчивается этапом Assert, который обычно включает одно или несколько конкретных утверждений. Это облегчает последовательную и объективную оценку результатов теста, даже простым просмотром логов.
Например, рассмотрим результаты теста из документации Mocha:
В результатах Mocha видно, что набор тестов «#indexOf()
» в коллекции «Array
» содержит два модульных теста. Изучив упавший тест, сделанный по шаблону AAA, вы сможете быстро определить, где нужно исправить проблему:
describe("Array", function () { describe("#indexOf()", function () { it("should return -1 when not present", function () { // arrange const inputArray = [1, 2, 3] // act const result = inputArray.indexOf(4) // assert expect(result).to.equal(1) }) }) // other test... })
Шаг Arrange не содержит ошибок. Аналогично, шаг Act выполнен как положено. Проблема заключается в фазе Assert, где отсутствует знак “-” перед 1.
Тест проверяет вывод неверного значения и, соответственно, падает. Можно пофиксить его так:
expect(result).to.equal(-1)
Понятность тестов важна для быстрого выявления и устранения проблем, что ускоряет процесс исправления неработающих деплоев.
Как уже было сказано, паттерн AAA способствует автоматизации модульных тестов.
Заключение
В этом руководстве вы узнали о паттерне Arrange-Act-Assert и о том, как выглядит юнит-тест, построенный с использованием этого подхода. Вы изучили шаги, которые паттерн рекомендует для разработки читаемых и поддерживаемых тестов, а также преимущества, которые он приносит в юнит-тестирование. Поддерживаемый сообществом и считающийся стандартом де-факто, паттерн AAA также является ценным инструментом автоматизации модульных тестов.