-
Notifications
You must be signed in to change notification settings - Fork 277
Catch2 unit tests
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:
- Use CTest to manage the test runs
- 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.) - 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.) - Additionally add separate targets for each test file, for quicker turnaround when testing code changes.
- 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 forClassName
, without having to wait for a rebuild of all of the other test executables, the SWIG bindings, the other executable targets, etc. - 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.
- After making a source change to the library, you can use
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.
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 aSUITE()
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 noSUITE()
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 theCV
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.)
(Catch2 also supports REQUIRE()
, which unlike CHECK()
will terminate the TEST_CASE()
on failure.)
-
UnitTest++ had
CHECK()
,CHECK_EQUAL()
, andCHECK_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.
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.
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()
.
- 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 useCHECK_THROWS_AS()
, which takes the same two arguments as UnitTest++'sCHECK_THROW()
and offers identical functionality. - (Don't overlook the change in tense: that's THROWS, with an "S".)
Want to make some OpenShot friends and join our open-source team? Please send me an email ([email protected]) and introduce yourself! All volunteers are welcome, regardless of skills and/or skill level. Let's build something amazing!