Введение
Традиционные юнит-тесты проверяют конкретные, заранее придуманные примеры входных данных («при a=2, b=3 ожидаем 5»). Property-based тестирование переворачивает подход: вместо конкретных примеров формулируется общее свойство, которое должно выполняться для любых входных данных («результат сложения всегда больше или равен каждому из слагаемых для неотрицательных чисел»), а фреймворк сам генерирует множество случайных входных данных, пытаясь найти контрпример, нарушающий свойство. RapidCheck — одна из библиотек, реализующих этот подход для C++, с интеграцией в QTest и другие фреймворки. В статье разберём базовое использование RapidCheck.
Концепция
В property-based тестировании задача разработчика — не придумать конкретные тестовые случаи, а сформулировать инвариант, который должен сохраняться для всего класса входных данных. RapidCheck генерирует случайные значения нужных типов (используя генераторы — встроенные для примитивных типов и контейнеров, либо кастомные для пользовательских типов), вызывает проверяемую функцию и проверяет заданное свойство через RC_ASSERT. При обнаружении нарушающего свойство примера RapidCheck автоматически пытается «сократить» (shrink) входные данные до минимального воспроизводящего проблему случая, что значительно облегчает диагностику по сравнению со случайным «сырым» входом.
Пример кода
#include <rapidcheck.h>
#include <vector>
#include <algorithm>
// Функция, которую тестируем: собственная реализация сортировки
std::vector<int> mySort(std::vector<int> input)
{
std::sort(input.begin(), input.end());
return input;
}
int main()
{
// Свойство 1: результат сортировки всегда отсортирован по возрастанию
rc::check("результат отсортирован по возрастанию",
[](const std::vector<int> &input) {
const auto sorted = mySort(input);
RC_ASSERT(std::is_sorted(sorted.begin(), sorted.end()));
});
// Свойство 2: сортировка не меняет количество элементов
rc::check("сортировка сохраняет размер вектора",
[](const std::vector<int> &input) {
const auto sorted = mySort(input);
RC_ASSERT(sorted.size() == input.size());
});
// Свойство 3: сортировка не теряет и не добавляет элементы (идемпотентность для мультимножества)
rc::check("сортировка дважды равна сортировке один раз",
[](const std::vector<int> &input) {
const auto sortedOnce = mySort(input);
const auto sortedTwice = mySort(sortedOnce);
RC_ASSERT(sortedOnce == sortedTwice);
});
return 0;
}
// Интеграция RapidCheck с QTest через rapidcheck/qt-extras (упрощённый пример)
#include <QtTest/QtTest>
#include <rapidcheck.h>
class TestSortProperties : public QObject
{
Q_OBJECT
private slots:
void testSortIsIdempotent()
{
const bool success = rc::check([](const std::vector<int> &input) {
auto once = input;
std::sort(once.begin(), once.end());
auto twice = once;
std::sort(twice.begin(), twice.end());
RC_ASSERT(once == twice);
});
QVERIFY(success);
}
};
QTEST_APPLESS_MAIN(TestSortProperties)
#include "test_sortproperties.moc"
Пояснения к коду
Каждый вызов rc::check() принимает строковое описание свойства и лямбду, параметром которой RapidCheck автоматически передаёт случайно сгенерированное значение нужного типа (в примерах — std::vector<int>, для которого у RapidCheck есть встроенный генератор, включая граничные случаи вроде пустого вектора). Внутри лямбды RC_ASSERT проверяет сформулированное свойство — если оно нарушается хотя бы для одного из множества сгенерированных входов, RapidCheck сообщает минимальный воспроизводящий пример после автоматического сокращения (shrinking).
Второй пример показывает интеграцию с QTest — rc::check() возвращает булево значение успеха всего набора случайных проверок, которое можно передать в обычный QVERIFY, сохраняя совместимость с привычной структурой QTest-тестов и CI-раннером ctest.
Подводные камни
- Property-based тестирование не заменяет, а дополняет example-based тесты. Конкретные граничные случаи, известные из требований или прошлых багов («регрессионные» тесты), стоит сохранять как отдельные явные тестовые случаи — генератор случайных данных не гарантированно «наткнётся» именно на исторически проблемный кейс при каждом запуске (если не зафиксировать seed).
- Формулирование слишком слабых свойств. Свойство «функция не крашится» — валидное, но малополезное свойство; ценность property-based тестов прямо зависит от того, насколько точно сформулированный инвариант отражает реальные ожидания от поведения функции.
- Недетерминированность запуска без фиксированного seed. По умолчанию RapidCheck использует случайный seed при каждом запуске, и тест, упавший один раз на конкретном случайном входе, может не воспроизвестись на следующем запуске — RapidCheck выводит seed обнаруженного сбоя, и его нужно зафиксировать вручную для воспроизведения проблемы при отладке.
- Написание кастомных генераторов для сложных пользовательских типов требует заметных дополнительных усилий по сравнению с встроенными типами — для проектов с богатой собственной доменной моделью это может стать барьером входа для команды, незнакомой с подходом.