Catch2 Matchers для C++: полное руководство с примерами
1. Что это и зачем нужна
Библиотека Catch2 (C++ Automated Test Cases in Headers) — это современный фреймворк для модульного тестирования на C++, который распространяется в виде одного заголовочного файла. Одной из его ключевых и мощных возможностей является система Matchers (матчеров) — декларативных средств проверки значений, которые делают тесты более читаемыми, гибкими и информативными.
В отличие от традиционных утверждений (assertions) в стиле REQUIRE(x == 5), матчеры позволяют формулировать сложные условия проверки в естественной, почти человекочитаемой форме. Например, вместо того чтобы писать цикл для проверки всех элементов вектора, вы можете использовать REQUIRE_THAT(vec, Catch::Matchers::AllMatch(Catch::Matchers::Predicate<int>([](int i) { return i > 0; }))). Это не только сокращает код, но и делает намерения тестировщика кристально ясными.
Зачем нужны матчеры? Во-первых, они обеспечивают высокую выразительность: вы можете проверять, что строка содержит подстроку, начинается с определённого префикса, или что число находится в заданном диапазоне, используя одну лаконичную конструкцию. Во-вторых, матчеры предоставляют детальную диагностику при падении теста — вместо сухого "false" вы получаете осмысленное сообщение, например: "Expected: starts with 'Hello' Actual: 'World'". В-третьих, они композируемы: вы можете комбинировать простые матчеры в сложные выражения с помощью логических операторов (And, Or, Not).
2. Установка
Catch2 — это библиотека с заголовочным файлом (header-only). Для её использования не требуется сложной установки или настройки сборки. Существует два основных способа:
Способ 1: Загрузка одного файла (рекомендуется для начала)
Скачайте файл catch_amalgamated.hpp или catch.hpp с официальной страницы релизов Catch2 на GitHub. Поместите его в папку с вашим проектом и включите в код:
#include "catch_amalgamated.hpp"
Способ 2: Использование системы управления зависимостями (vcpkg, Conan, CMake FetchContent)
Для профессиональных проектов рекомендуется использовать менеджер пакетов. Пример для vcpkg:
# Установите Catch2 через vcpkg
vcpkg install catch2
Затем в вашем CMakeLists.txt:
find_package(Catch2 REQUIRED)
target_link_libraries(my_test PRIVATE Catch2::Catch2WithMain)
Важно: В версиях Catch2 v3.x и выше, матчеры находятся в пространстве имён Catch::Matchers. В версии v2.x они были в Catch::Matchers (то же самое, но структура заголовочных файлов изменилась). Для v3.x используйте #include <catch2/catch_test_macros.hpp> и #include <catch2/matchers/catch_matchers_all.hpp>.
3. Быстрый старт — минимальный рабочий пример
Давайте напишем простейший тест с использованием матчера для проверки строки:
// test.cpp
#define CATCH_CONFIG_MAIN // Эта строка должна быть только в одном .cpp файле
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_all.hpp>
#include <string>
TEST_CASE("String matchers work", "[string]") {
std::string hello = "Hello, World!";
// Проверяем, что строка содержит подстроку
CHECK_THAT(hello, Catch::Matchers::ContainsSubstring("World"));
// Проверяем, что строка начинается с "Hello"
CHECK_THAT(hello, Catch::Matchers::StartsWith("Hello"));
// Проверяем, что строка заканчивается на "!"
CHECK_THAT(hello, Catch::Matchers::EndsWith("!"));
}
Скомпилируйте и запустите:
g++ -std=c++17 test.cpp -o test
./test
Вы увидите зелёный отчёт о прохождении всех трёх проверок. Если какая-то проверка не пройдёт, Catch2 выдаст подробное сообщение об ошибке.
4. Основные методы/функции/классы
В этом разделе мы рассмотрим 8 ключевых матчеров, которые покрывают 90% потребностей при тестировании. Все примеры предполагают использование Catch2 v3.x.
4.1. StartsWith — Проверка начала строки
Сигнатура:
MatcherBase<std::string> StartsWith(const std::string& prefix);
Что делает: Проверяет, что строка начинается с заданного префикса. Чувствителен к регистру.
Пример:
TEST_CASE("StartsWith") {
std::string url = "https://example.com";
CHECK_THAT(url, Catch::Matchers::StartsWith("https"));
// Следующий тест упадёт, так как регистр не совпадает
// CHECK_THAT(url, Catch::Matchers::StartsWith("HTTP"));
}
4.2. EndsWith — Проверка окончания строки
Сигнатура:
MatcherBase<std::string> EndsWith(const std::string& suffix);
Что делает: Проверяет, что строка заканчивается заданным суффиксом.
Пример:
TEST_CASE("EndsWith") {
std::string filename = "report.pdf";
CHECK_THAT(filename, Catch::Matchers::EndsWith(".pdf"));
}
4.3. ContainsSubstring — Проверка вхождения подстроки
Сигнатура:
MatcherBase<std::string> ContainsSubstring(const std::string& substring);
Что делает: Проверяет, содержит ли строка указанную подстроку.
Пример:
TEST_CASE("ContainsSubstring") {
std::string message = "Error: file not found";
CHECK_THAT(message, Catch::Matchers::ContainsSubstring("file not found"));
CHECK_THAT(message, Catch::Matchers::ContainsSubstring("Error"));
}
4.4. WithinAbs — Проверка чисел с плавающей точкой (абсолютная погрешность)
Сигнатура:
MatcherBase<double> WithinAbs(double target, double margin);
Что делает: Проверяет, что число находится в пределах заданной абсолютной погрешности от целевого значения. Идеально подходит для сравнения чисел с плавающей точкой.
Пример:
TEST_CASE("WithinAbs") {
double result = 3.14159;
CHECK_THAT(result, Catch::Matchers::WithinAbs(3.14, 0.01)); // Пройдёт: |3.14159 - 3.14| = 0.00159 < 0.01
CHECK_THAT(result, Catch::Matchers::WithinAbs(3.14, 0.001)); // Упадёт: 0.00159 > 0.001
}
4.5. WithinRel — Проверка чисел с плавающей точкой (относительная погрешность)
Сигнатура:
MatcherBase<double> WithinRel(double target, double epsilon = std::numeric_limits<double>::epsilon() * 100);
Что делает: Проверяет, что относительная разница между значением и целью меньше заданного эпсилон. Полезно для сравнения чисел разного масштаба.
Пример:
TEST_CASE("WithinRel") {
double big = 1e10;
double small = 1e-10;
CHECK_THAT(big * 1.0001, Catch::Matchers::WithinRel(big, 0.001)); // Пройдёт: относительная ошибка ~0.01%
CHECK_THAT(small * 2.0, Catch::Matchers::WithinRel(small, 0.5)); // Пройдёт: ошибка 100% < 50%? Нет, упадёт.
}
4.6. Predicate — Пользовательские проверки
Сигнатура:
MatcherBase<T> Predicate(const std::string& description, PredicateFn fn);
Что делает: Позволяет создать матчер на основе любой пользовательской функции (предиката). Вы сами описываете, что значит "совпадение".
Пример:
TEST_CASE("Predicate") {
auto is_even = [](int i) { return i % 2 == 0; };
CHECK_THAT(42, Catch::Matchers::Predicate<int>(is_even, "is even"));
CHECK_THAT(7, !Catch::Matchers::Predicate<int>(is_even, "is even")); // Используем отрицание
}
4.7. AllMatch — Проверка всех элементов диапазона
Сигнатура:
MatcherBase<RangeLike> AllMatch(MatcherBase<T> innerMatcher);
Что делает: Применяет внутренний матчер к каждому элементу контейнера (вектора, списка, массива). Тест проходит, только если все элементы удовлетворяют условию.
Пример:
TEST_CASE("AllMatch") {
std::vector<int> positive_numbers = {1, 2, 3, 4, 5};
auto is_positive = Catch::Matchers::Predicate<int>([](int i) { return i > 0; }, "is positive");
CHECK_THAT(positive_numbers, Catch::Matchers::AllMatch(is_positive));
std::vector<int> mixed = {1, -2, 3};
// CHECK_THAT(mixed, Catch::Matchers::AllMatch(is_positive)); // Упадёт, так как -2 не положительное
}
4.8. AnyMatch — Проверка хотя бы одного элемента диапазона
Сигнатура:
MatcherBase<RangeLike> AnyMatch(MatcherBase<T> innerMatcher);
Что делает: Противоположность AllMatch. Тест проходит, если хотя бы один элемент контейнера удовлетворяет внутреннему матчеру.
Пример:
TEST_CASE("AnyMatch") {
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
auto starts_with_a = Catch::Matchers::StartsWith("A");
CHECK_THAT(names, Catch::Matchers::AnyMatch(starts_with_a)); // Alice подходит
auto starts_with_z = Catch::Matchers::StartsWith("Z");
// CHECK_THAT(names, Catch::Matchers::AnyMatch(starts_with_z)); // Упадёт
}
5. Реальный практический пример (100-200 строк кода)
Представим, что мы разрабатываем простую систему управления библиотекой. У нас есть класс Book и класс Library. Мы хотим протестировать функциональность поиска и фильтрации книг.
#define CATCH_CONFIG_MAIN
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_all.hpp>
#include <string>
#include <vector>
#include <algorithm>
struct Book {
std::string title;
std::string author;
int year;
bool is_available;
};
class Library {
public:
void add_book(const Book& book) {
books_.push_back(book);
}
std::vector<Book> find_by_author(const std::string& author) const {
std::vector<Book> result;
std::copy_if(books_.begin(), books_.end(), std::back_inserter(result),
[&](const Book& b) { return b.author == author; });
return result;
}
std::vector<Book> find_available_after_year(int year) const {
std::vector<Book> result;
std::copy_if(books_.begin(), books_.end(), std::back_inserter(result),
[year](const Book& b) { return b.is_available && b.year > year; });
return result;
}
const std::vector<Book>& all_books() const { return books_; }
private:
std::vector<Book> books_;
};
TEST_CASE("Library book search", "[library]") {
Library lib;
lib.add_book({"The C++ Programming Language", "Bjarne Stroustrup", 2013, true});
lib.add_book({"Effective Modern C++", "Scott Meyers", 2014, true});
lib.add_book({"Design Patterns", "Gang of Four", 1994, false});
lib.add_book({"C++ Concurrency in Action", "Anthony Williams", 2019, true});
SECTION("Find by author") {
auto books_by_stroustrup = lib.find_by_author