При тестирования кода перед разработчиком сразу возникает два вопроса:
- Сколько тест кейсов нужно написать чтобы покрыть ими весь функционал;
- Какие значения необходимо подавать на вход тестируемой функции.
Ответы на них необходимо искать с использованием классов эквивалентности.
Классы эквивалентности — это техника проектирования тестов, при которой входные данные разбиваются на группы (классы), внутри которых программа должна вести себя одинаково. Поэтому достаточно протестировать один представитель из каждого класса, вместо проверки всех возможных значений.
Эта техника называется Equivalence Partitioning и используется в модульных, интеграционных и функциональных тестах.
В основе классов эквивалентности лежит следующая идея. Если один элемент класса работает правильно, то остальные элементы этого класса, вероятно, тоже будут работать правильно. Поэтому тестировать каждое значение не нужно. Мы делим входные данные на валидные и невалидные классы.
Рассмотрим следующий пример. Необходимо протестировать функцию calculateDiscount(sum), которая возвращает величину скидки, зависящую от суммы, потраченной покупателем, в соответствии со следующими правилами:
- если sum < 0, то выдать ошибку или сгенерировать исключение;
- если sum от 0 до 100, то величина скидки - 0%;
- если sum от 100 до 1000, то величина скидки скидка - 5%;
- если sum свыше 1000, то скидка 10%.
На основе этих правил введем следующие классы эквивалентности для переменной sum:
- sum < 0
- 0 ≤ sum <100
- 100 ≤ sum ≤ 1000
- sum > 1000
Каков минимальный набор тестов для этих классов? Необходимо создать по одному тесту на каждый класс эквивалентности, в которых значение sum находится в пределах класса, например:
- sum = -10
- sum = 50
- sum = 200
- sum = 2000
Таким образом, нужно написать минимум 4 теста, однако этого обычно не достаточно. Причина состоит в том, что многие ошибки находятся на границах диапазонов значений. Для диапазона с двумя значениями min и max необходимы следующие тесты:
- min-1
- min
- min+1
- max-1
- max
- max+1
Кроме того, если в коде функции встречается условный оператор, то каждое ветвление нужно пройти хотя бы один раз.
Также необходимо создавать тесты на каждую ошибку или исключение, генерируемое функцией, и на невалидные данные, которые могут подаваться на вход.
Таким образом, при написании модульных (unit) тестов необходимо выполнить следующие шаги:
- Разбить входные параметры функции на классы эквивалентности и написать минимум по одному тесту для каждого класса;
- Добавить тесты на граничные значения;
- Создать тесты на покрытие всех ветвей if/else функции;
- Добавить тесты на генерацию ошибок или исключений.
Хороший unit-тест должен отвечать следующим требованиям:
• быть коротким;
• проверять одно поведение;
• быть детерминированным;
• быть независимым от результатов других тестов, а также от очередности запуска тестов.
Каждый тест должен делать одну проверку, которая, в случае неудачи, сразу показывает причину падения.
Например, возьмем тест, проверяющий сразу 3 утверждения: имя пользователя - "John", ему 30 лет, двух буквенный код его страны - "DE".
assertEquals(user.getName(), "John");
assertEquals(user.getAge(), 30);
assertEquals(user.getCountry(), "DE");
Этот тест не соответствует описанному выше критерию, так как если одна из проверок закончится неудачей, то нужно будет открывать код теста и смотреть что именно пошло не так как запланировано.
Лучше разбить этот тест на 3, каждый из которых осуществляет только одну проверку:
testUserName()
testUserAge()
testUserCountry()
В совокупности с правильными названиями такие тесты при неудачном выполнении сразу делают очевидной причину.
Тесты должны читаться как документация, т.е. название теста должно определять его поведение.
При написании модульных тестов рекомендуется придерживаться стандартного паттерна структурирования модульных тестов Arrange–Act–Assert (в дальнейшем для краткости назовем его AAA), который помогает сделать их чистыми, понятными и легко поддерживаемыми.
Суть метода заключается в разделении каждого теста на три фазы:
1. Arrange (Подготовка)
Здесь вы настраиваете всё необходимое для выполнения теста. Например, создаете объекты, инициализируете переменные, настраиваете моки (заглушки) или подготавливаете базу данных.
Цель: привести систему в нужное начальное состояние.
2. Act (Действие)
Вызываете конкретный метод или функцию, которую хотите протестировать. Обычно это всего одна строка кода, представляющая собой основной сценарий теста.
Цель: выполнить действие и получить результат.
3. Assert (Проверка)
Сравниваете полученный результат с ожидаемым. Проверяете состояние объектов после действия или факт вызова определенных функций/методов класса.
Цель: подтвердить, что код сработал правильно. Если проверка не проходит, тест считается упавшим.
Такая практика тестирования имеет следующие преимущества:
- Читаемость: любой разработчик сразу видит, что настраивается, что тестируется и что проверяется.
- Единообразие: все тесты в проекте выглядят одинаково, что упрощает их ревью и доработку.
- Фокус внимания: структура тестов подталкивает к тому, чтобы один тест выполнял ровно одну проверку.
Вот пример теста функции Add, возвращающей сумму двух целых чисел, написанный в соответствии с ААА с использованием фреймворка Google Test, о котором я рассказывал в одной из предыдущих статей.
TEST(TestSuiteName, TestName) {
// 1. Arrange (Подготовка)
// Инициализация объектов, настройка входных данных.
int a = 5;
int b = 10;
Calculator calc;
// 2. Act (Действие)
// Вызов целевого метода, который мы тестируем.
int result = calc.Add(a, b);
// 3. Assert (Проверка)
// Сравнение полученного результата с ожидаемым.
EXPECT_EQ(result, 15);
}
При написании тестов важно придерживаться следующих рекомендаций:
- Arrange. Если подготовка данных повторяется для многих тестов, вынесите её в метод SetUp() класса testing::Test. Это позволит держать тело теста коротким.
- Act. Старайтесь, чтобы это была одна строка. Если "действие" проверки занимает 5-10 строк, возможно, метод делает слишком много или тест пере усложнен.
- Assert. В Google Test используйте ASSERT_* (прерывает тест при неудачной проверке) или EXPECT_* (продолжает выполнение). Хорошей практикой считается один Assert на тест.
В заключении рассмотрим тестирование функции вычисления скидки.
Вначале пишем тесты для условия sum < 0.
Далее тесты c проверкой границ для условия sum < 100
И, наконец, тесты с проверкой пограничных условий для sum <= 1000.
Обратите внимание, что тест Above1000 проверяет последний оператор return функции calculateDiscount.
Таким образом, мы написали тесты на все классы эквивалентности и проверили граничные значения. Исходный код примера можно посмотреть здесь. Продолжение следует...