Catch2 Matchers для C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                    

Рекомендаций