Catch is a unit testing framework that has some interesting (better!) ways to write tests for C and C++.
Instead of naming your tests with function calls, you can write your tests as a nested series of Given-When-Then statements. This makes it easier to understand what the heck your tests are actually doing. These are tests writting in the style of behavior-driven development (BDD).
For example, imagine that we're writing tests for the power switch on your computer. I could write a test function like this:
void
test_GivenThePowerIsOn_WhenThePowerButtonIsHeld_ThenThePowerIsOff()
{
// Test goes here.
}
But with Catch, I can write that same test like this:
SCENARIO("Power on tests", "[power_button]")
{
GIVEN("the power is on")
{
// Turn the power on here.
WHEN("the power button is held")
{
// Hold the button here
THEN("the power turns off")
{
// Check that the power is off now.
}
}
}
}
Wow... isnt' that a lot clearer? These tests are written as actual English phrases (with spaces between the words!).
Each "given," "when," or "then" clause also gets its own line, so that when these start to get long, there is still a pretty good chance I won't need to scroll to the right in my editor to read everything.
But wait, it gets better. You can also add other clauses for testing other paths through the scenario:
SCENARIO("Power on tests", "[power_button]")
{
GIVEN("the power is on")
{
// Turn the power on here.
WHEN("the power button is held")
{
// Hold the button here
THEN("the power turns off")
{
// Check that the power is off now.
}
}
WHEN("the power button is momentarily pressed")
{
// Momentarily press the button here
THEN("the power turns remains on")
{
// Check that the power is still on.
}
}
}
}
When this is run, Catch re-executes the stuff in the "given" clause for each of the "when" clauses. This lets us reuse our "givens" for multiple "whens." When we start to get a lot of tests, this will let us arrange things to reduce duplication. Now it's really starting to shine.
When there are errors, you also see the case that failed in plain English:
-------------------------------------------------------------------
Scenario: Power on tests
Given: the power is on
When: the power button is held down
Then: the power turns off
-------------------------------------------------------------------
test_power_button.c:26
...................................................................
test_power_button.c:43: FAILED:
REQUIRE( power_button_getPowerState() == POWER_OFF )
with expansion:
1 == 0
===================================================================
test cases: 2 | 1 passed | 1 failed
assertions: 4 | 3 passed | 1 failed
Setting up Catch to test your C
Let's look at how to make this work. You can find this full, working example on GitHub.
Catch is actually a C++ framework, but you can easily use it to test C applications just by compiling with g++ instead of gcc. This of course is only for running the tests on our host PC. When we want to compile for the target, we'll use a C cross-compiler (and won't build the tests).
Catch comes in a single C++ header file, which you can just drop somewhere into your project.
Catch will provide a main function for you so you can build a binary executable. You do this defining CATCH_CONFIG_MAIN
before including the header file. You only need to do this once, and the recommendation is to do it in a separate file. Here is the complete test_main.c:
#define CATCH_CONFIG_MAIN // This tells Catch to provide a main()
#include "catch.hpp"
The reason for separating this is so that we can build an object file for this and reuse it without having to recompile it each time we build.
Then, you put your tests into their own file. In this case, test_power_button.c is used to test the power_button module. The test file needs to include the catch header:
#include "catch.hpp"
#include "power_button.h"
#include <string.h>
SCENARIO("Power off tests", "[power_button]")
{
GIVEN("the power is off")
{
power_button_initialize(POWER_OFF);
WHEN("nothing happens")
{
THEN("the power is still off")
{
REQUIRE(power_button_getPowerState() == POWER_OFF);
}
}
WHEN("the power button is momentarily pressed")
{
power_button_pressMomentary();
THEN("the power turns on")
{
REQUIRE(power_button_getPowerState() == POWER_ON);
}
}
}
}
SCENARIO("Power on tests", "[power_button]")
{
GIVEN("the power is on")
{
power_button_initialize(POWER_OFF);
power_button_pressMomentary(); // Turn the power on.
WHEN("the power button is momentarily pressed")
{
power_button_pressMomentary();
THEN("the power remains on")
{
REQUIRE(power_button_getPowerState() == POWER_ON);
}
}
WHEN("the power button is held down")
{
power_button_pressHold();
THEN("the power turns off")
{
REQUIRE(power_button_getPowerState() == POWER_OFF);
}
}
}
}
And here is the module that we're testing, power_button.c:
/*
Implements the behavior of a PC power button.
*/
#include "power_button.h"
static PowerState state = POWER_OFF;
void power_button_initialize(PowerState initalState)
{
state = initalState;
}
PowerState power_button_getPowerState(void)
{
return state;
}
void power_button_pressMomentary(void)
{
state = POWER_ON;
}
void power_button_pressHold(void)
{
state = POWER_OFF;
}
To build this, every C file gets compiled to it's own object file:
g++ -c power_button.c
g++ -c test_main.c
g++ -c test_power_button.c
And each object file is linked together to create a catch binary:
g++ power_button.o test_main.o test_power_button.o -o catch.exe
Then we can simply run catch.exe to execute the tests.
The catch binary also has bunch of fun built-in command line options. Theses allow you to do things like only running specific tests. I'd encourage you to check out the Catch documentation.
The only issue I've had so far is that the tests take a little longer to compile than I'd expect. The key to managing this though is to use a separate test_main.c (as described above) and use incremental compilation.
All in all, Catch looks like another promising tool that might help to write some clearer tests.