Catch2#
Proste testy#
Podstwowym makrem definiującym noty test w Catch2 jest TEST_CASE.
Pierwszym argumentem makra TEST_CASE jest nazwa testu jest dowolnym stringiem, który opisuje test. Drugi (opcjonalny) argument to tag, który pozwala na grupowanie testów.
Do sprawdzenia warunków używamy makr REQUIRE i CHECK. Różnica między nimi polega na tym, że REQUIRE kończy test, jeśli warunek nie jest spełniony, a CHECK kontynuuje test.
unsigned int Factorial(unsigned int number) {
return number <= 1 ? number : Factorial(number-1)*number;
}
TEST_CASE("Factorials are computed", "[factorial, math]")
{
REQUIRE( Factorial(0) == 1 );
REQUIRE( Factorial(1) == 1 );
REQUIRE( Factorial(2) == 2 );
REQUIRE( Factorial(3) == 6 );
REQUIRE( Factorial(10) == 3628800 );
}
Testy i sekcje#
Testy mogą być zagnieżdżone w sekcje. Sekcje pozwalają na grupowanie testów i wykonywanie ich w określonej kolejności. Odpowiada to mechanizmowi fikstur w innych frameworkach testowych.
TEST_CASE("vectors can be sized and resized", "[vector]")
{
std::vector<int> v(5); // This setup will be done 4 times in total, once for each section
REQUIRE(v.size() == 5);
REQUIRE(v.capacity() >= 5);
SECTION("resizing bigger changes size and capacity")
{
v.resize(10);
REQUIRE(v.size() == 10);
REQUIRE(v.capacity() >= 10);
}
SECTION("resizing smaller changes size but not capacity")
{
v.resize(0);
REQUIRE(v.size() == 0);
REQUIRE(v.capacity() >= 5);
}
SECTION("reserving bigger changes capacity but not size")
{
v.reserve(10);
REQUIRE(v.size() == 5);
REQUIRE(v.capacity() >= 10);
}
SECTION("reserving smaller does not change size or capacity")
{
v.reserve(0);
REQUIRE(v.size() == 5);
REQUIRE(v.capacity() >= 5);
}
}
Dla każdej sekcji SECTION test jest wykonywany od początku. Oznacza to, że każda sekcja jest uruchamiana z nowo skonstruowanym wektorem v.
Sekcje mogą być zagnieżdżane, co pozwala na tworzenie bardziej złożonych struktur testów.
SECTION( "reserving bigger changes capacity but not size" )
{
v.reserve(10);
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
SECTION( "reserving down unused capacity does not change capacity" )
{
v.reserve( 7 );
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
}
Testy w stylu BDD#
Catch2 pozwala na pisanie testów w stylu BDD (Behavior-Driven Development). W tym stylu testy są pisane w sposób bardziej czytelny dla osób niebędących programistami. Poprzez odpowiednie aliasy dla makr TEST_CASE i SECTION można tworzyć testy wykorzystujące styl nazewnictwa Given-When-Then.
#include <catch2/catch_test_macros.hpp>
SCENARIO( "vectors can be sized and resized", "[vector]" ) {
GIVEN( "A vector with some items" ) {
std::vector<int> v( 5 );
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
WHEN( "the size is increased" ) {
v.resize( 10 );
THEN( "the size and capacity change" ) {
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "the size is reduced" ) {
v.resize( 0 );
THEN( "the size changes but not capacity" ) {
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
}
WHEN( "more capacity is reserved" ) {
v.reserve( 10 );
THEN( "the capacity changes but not the size" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "less capacity is reserved" ) {
v.reserve( 0 );
THEN( "neither size nor capacity are changed" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
}
}
Asercje#
Catch2 dostarcza kilka makr do sprawdzania warunków.
REQUIRE i CHECK#
Makra REQUIRE testują wyrażenie i przerywają test jeśli wyrażenie nie jest ewaluowane do true. Rodzina makr CHECK testuje wyrażenie i kontynuuje test, nawet jeśli wyrażenie nie jest prawdziwe.
CHECK( str == "string value" );
CHECK( thisReturnsTrue() );
REQUIRE( i == 42 );
Wyrażenia logiczne zawierające operatory && oraz || nie mogą być zdekomponowane przez Catch2 i ich kompilacja zwróci błąd. Aby użyć takich wyrażeń, należy:
Zastosować nawiasy, aby wyrażenie było ewaluowane jako pojedyncza wartość logiczna przed dekompozycją.
REQUIRE( (a == 1 && b == 2) );
Przepisać wyrażenie
REQUIRE(a == 1 && b == 2)do postaci:REQUIRE( a == 1 ); REQUIRE( b == 2 );
REQUIRE_FALSE#
Wyrażenia poprzedzone operatorem ! nie mogą być zdekomponowane przez Catch2. W takim przypadku można użyć makra REQUIRE_FALSE.
Status result = someFunction();
REQUIRE_FALSE(result); // result must evaluate to false, and Catch2 will print
// out the value of ret if possibly
Asercje wyjątków#
Catch2 pozwala na testowanie wyjątków.
REQUIRE_THROWSsprawdza, czy wyjątek został rzuconymakro
REQUIRE_THROWS_ASsprawdza, czy wyjątek został rzucony i jest odpowiedniego typuREQUIRE_THROWS_WITHsprawdza, czy wyjątek został rzucony i ma odpowiedni komunikatREQUIRE_NOTHROWsprawdza, czy wyjątek nie został rzucony
std::vector<int> v = {1, 2, 3};
REQUIRE_THROWS( v.at(3) );
REQUIRE_THROWS_AS( v.at(3), std::out_of_range );
REQUIRE_THROWS_WITH( v.at(3), "out of range" );
REQUIRE_NOTHROW( v.at(2) );
Asercje liczb zmiennoprzecinkowych#
Catch2 dostarcza makra do porównywania liczb zmiennoprzecinkowych. Zalecanym sposobem porównania liczb zmiennoprzecinkowych jest wykorzystanie makr dopasowujących tzw. matchers.
#include <catch2/matchers/catch_matchers_floating_point.hpp>
Biblioteka dostarcza trzy makra:
WithinAbs(double target, double margin)- sprawdza, czy wartość mieści się w określonym przezmarginprzedzialeREQUIRE_THAT(1.0, WithinAbs(1.2, 0.2));
WithinRel(FloatingPoint target, FloatingPoint eps)- akceptuje porównanie, jeżeli wartość jest w przybliżeniu równa wartości oczekiwanej z tolerancjąeps. Sprawdzany jest warunek|arg - target| <= eps * max(|arg|, |target|). Jeżeli nie podajemyeps, to domyślnie jest tostd::numeric_limits<FloatingPoint>::epsilon * 100REQUIRE_THAT(1.0, WithinRel(1.0, 0.0001));
WithinULP(FloatingPoint target, uint64_t maxUlpDiff)- tworzy porównanie, które akceptuje wartość, jeżeli różnica między wartością oczekiwaną a wartością testowaną jest mniejsza niżmaxUlpDiffjednostek ULP.REQUIRE_THAT( -0.f, WithinULP( 0.f, 0 ) );
Testy parametryzowane#
Generatory#
Testy parametryzowane pozwalają na przetestowanie kodu z różnymi zestawami danych wejściowych. Catch2 wykorzystuje generatory, które pozwalają przekazać zestaw danych do testu - respektowane są zagnieżdżenia makr TEST_CASE i SECTION.
TEST_CASE("is_odd")
{
auto n = GENERATE(1, 3, 5);
REQUIRE(is_odd(n));
}
Jeśli chcemy przekazać do testu parametryzowanego kilka wartości jednocześnie należy użyć makra GENERATE w połączeniu z generatorem table.
Aby utworzyć sekcję z dynamiczną nazwą odwołującą się do parametrów, należy użyć makra DYNAMIC_SECTION.
TEST_CASE("table generators", "[generators]")
{
auto [text, length] = GENERATE(table<std::string, size_t>({
{"a", 1},
{"bb", 2},
{"ccc", 3}
}));
DYNAMIC_SECTION("length for " << text << " is " << length)
{
REQUIRE(text.length() == length);
}
}
W przypadku testów w stylu BDD parametryzacja może wyglądać następująco:
SCENARIO("Eating cucumbers", "[approvals]")
{
auto [start, eat, left] = GENERATE(table<int, int, int>({
{12, 5, 7},
{20, 2, 18},
{3, 1, 2}
}));
auto eat_cucumber = [](int start, int eat) { return start - eat; };
GIVEN("there are " << start << " cucumbers")
WHEN("I eat " << eat << " cucumbers")
THEN("I should have " << left << " cucumbers")
{
REQUIRE(eat_cucumber(start, eat) == left);
}
}
Parametryzacja testów typami#
Catch2 pozwala na parametryzację testów typami. W tym celu należy użyć makra TEMPLATE_TEST_CASE.
TEMPLATE_TEST_CASE("vector can be resized", "[template]", int, std::string, float)
{
std::vector<TestType> vec; // This setup will be done 3 times in total, once for each type
CHECK(vec.size() == 0);
vec.resize(10);
REQUIRE(vec.size() == 10);
}
Benchmarki#
Catch2 pozwala na tworzenie benchmarków. Aby utworzyć benchmark, należy użyć makra BENCHMARK.
Najprostszy benchmark wykorzystuje makro
BENCHMARK:
TEST_CASE("Fibonacci")
{
CHECK(Fibonacci(0) == 1);
// some more asserts..
CHECK(Fibonacci(5) == 8);
// some more asserts..
// now let's benchmark:
BENCHMARK("Fibonacci 20") {
return Fibonacci(20);
};
BENCHMARK("Fibonacci 25") {
return Fibonacci(25);
};
BENCHMARK("Fibonacci 30") {
return Fibonacci(30);
};
BENCHMARK("Fibonacci 35") {
return Fibonacci(35);
};
}
Zaawansowane benchmarki wykorzystują makro
BENCHMARK_ADVANCED:
std::vector<int> generate_data()
{
size_t size = 1000;
std::random_device rd{};
std::mt19937 rnd_engine{rd()};
std::uniform_int_distribution<> rnd_gen(0, 10'000);
std::vector<int> vec(size);
std::generate_n(begin(vec), size, [&] { return rnd_gen(rnd_engine); });
return vec;
}
TEST_CASE("sorting", "[benchmarks]")
{
BENCHMARK_ADVANCED("sort - stl")(Catch::Benchmark::Chronometer meter) {
auto data = generate_data(); // setup data for benchmark
meter.measure([&data] { std::sort(data.begin(), data.end()); }); // this will be benchmarked
};
BENCHMARK_ADVANCED("sort - ranges")(Catch::Benchmark::Chronometer meter) {
auto data = generate_data(); // setup data for benchmark
meter.measure([&data] { std::ranges::sort(data); }); // this will be benchmarked
};
}