Skip to content

Catch2 unit tests

Frank Dana edited this page Sep 11, 2021 · 2 revisions

Recent changes to the repo's unit testing infrastructure

libopenshot previously used the UnitTest++ framework (which has been in a state of suspended development for some time now), so we've recently replaced it with Google's Catch2 framework.

In the process of making that change, the testing infrastructure was also updated to:

  1. Use CTest to manage the test runs
  2. Run CTest in parallel by default, when building --target coverage. (Somewhat confusingly, that target can be used whether or not coverage is enabled. It will collect coverage if told to, won't collect it otherwise, but will always run the tests with our preferred configuration. The automatic CMake --target test doesn't do that.)
  3. Build a separate test executable openshot-FooClass-test for each source file. (Yay! No more test cases stomping all over each other and messing up the results.)
  4. Additionally add separate targets for each test file, for quicker turnaround when testing code changes.
    1. After making a source change to the library, you can use cmake --build build --target ClassName_coverage to rebuild libopenshot and re-run the tests for ClassName, without having to wait for a rebuild of all of the other test executables, the SWIG bindings, the other executable targets, etc.
    2. Or, use cmake --build build --target openshot-ClassName-test to rebuild just the library and the single unit test executable, but without it being run automatically.

Writing tests for Catch2

From the perspective of writing unit tests, Catch2 is very similar to UnitTest++. Most UnitTest++ macros have a direct mapping into equivalent Catch2 syntax, and only minor adjustments to syntax are required to switch from one to the other. In fact, the conversions in this PR were largely done by regular expression replacements run over the entire tests/ directory, plus some manual cleanup to fix cases my initial expressions didn't properly account for.

What follows is a rough guide to how our old UnitTest++ code can be / has been translated into Catch2 code. It covers only the UnitTest++ features we actually made use of. Catch2 also offers additional functionality beyond what's required to provide a subset of UnitTest++'s features, and anyone adding to our test codebase should feel free to make use of any of those features in structuring new or existing tests. See the Catch2 documentation for complete details.

Converting UnitTest++ code into Catch2 code

TEST() becomes TEST_CASE(), with significantly different syntax

Calls to the TEST() macro from UnitTest++ can generally be replaced with TEST_CASE() calls in Catch2. However, the syntax is quite different between the two, more than any other change here.

  • In UnitTest++, TEST() takes a class/function name as test identifier, e.g. TEST(Default_Constructor). If the names are wrapped in a SUITE() identifier, they need not be unique between files, only within a given file.

  • In Catch2, the TEST_CASE() macro accepts test names as free-form strings, which must be double-quoted but can contain spaces and punctuation. There is no SUITE() macro, however because we are building separate executables for each source file, the names need not be unique between files. TEST_CASE() also accepts a second argument, a string containing a list of tags applied to each test. Each tag is enclosed in square braces, and any number of tags can be applied by concatenating them. I have tagged each of the existing tests with [libopenshot] and the tested class name (e.g. [frame] or [imagereader]), and have additionally tagged [opencv] on any tests involving the CV code.

Example:

// Before, in tests/FrameMapper_Tests.cpp
SUITE(FrameMapper) {
TEST(Default_Constructor)
{
  …
}
}

// After, in tests/FrameMapper.cpp
TEST_CASE("default constructor", "[libopenshot][framemapper]")
{
  …
}

(However, because the macros were modified by regular expressions, most of them are currently of the form TEST_CASE("Default_Constructor", …) — this should not be interpreted as a canonical or preferred formatting, and anyone should feel free to write new TEST_CASE()s in Catch2 style, as well as to submit PRs updating the names for existing tests should they feel so inclined.)

For the most part, the only test macro needed is: CHECK(), which takes a single argument.

(Catch2 also supports REQUIRE(), which unlike CHECK() will terminate the TEST_CASE() on failure.)

  • UnitTest++ had CHECK(), CHECK_EQUAL(), and CHECK_CLOSE(), which took one, two, and three arguments respectively.

  • Catch2 replaces all of those with single-argument macros, which take simple boolean comparisons: a == 4, p != nullptr, pi < 3.15, etc. Nothing more complex — no boolean algebra, nothing involving && or ||. The test must be a single boolean operation.

Examples:

// Before
CHECK_EQUAL(true, r.info.has_video);
CHECK_EQUAL(1, f.number);
CHECK_EQUAL(openshot::InterpolationType::BEZIER, p.interpolation);
// After
CHECK(r.info.has_video == true);
CHECK(f.number == 1);
CHECK(p.interpolation == openshot::InterpolationType::BEZIER);

Pretty straightforward.

A negated form, CHECK_FALSE(), can optionally be used to handle certain specific cases

The only other macro used for comparisons is CHECK_FALSE() / REQUIRE_FALSE(), necessary because CHECK() is incompatible with logically negated expressions. So:

// instead of
CHECK(!haz_cheezburger);  // fails
// you can write
CHECK_FALSE(haz_cheezburger);  // passes

The above could also be written CHECK(haz_cheezburger == false), tho, so you never have to use CHECK_FALSE(). Still, it's there if you prefer. Either way, the important thing to bear in mind is that CHECK(!something); won't work.

Instead of CHECK_CLOSE(), use the Approx() helper in CHECK() comparisons on floating-point values

To handle imprecise floating-point checks, Catch2 provides an Approx() helper class that can be used to perform fuzzy comparisons with float values. Approx() offers three methods for setting the required precision for the comparison, the most directly equivalent to CHECK_CLOSE() being Approx().margin().

Example:

// Before
CHECK_CLOSE(29.97, r->info.fps, 0.01);
// After
CHECK(r->info.fps == Approx(29.97).margin(0.01));

See the documentation for details on the other two methods, .epsilon() and .scale().

CHECK_THROW() becomes CHECK_THROWS_AS()

  • UnitTest++ tests for exceptions using CHECK_THROW(expression, exception_class).
  • Catch2 has a CHECK_THROW(expression), which only checks that the expression throws any exception. Generally you should instead use CHECK_THROWS_AS(), which takes the same two arguments as UnitTest++'s CHECK_THROW() and offers identical functionality.
  • (Don't overlook the change in tense: that's THROWS, with an "S".)