Have you ever written a test which mostly consists of the same block of code over and over again with different parameters?
// paraphrased from projects/charger/test/test_control_pilot.c void test_various_duty_cycles(void) { uint16_t answer = 0; uint16_t actual = 0; // case 1 s_duty_cycle = 10; answer = 0; actual = control_pilot_get_current(); TEST_ASSERT_EQUAL(answer, actual); // case 2 s_duty_cycle = 97; answer = 60; actual = control_pilot_get_current(); TEST_ASSERT_EQUAL(answer, actual); // case 3 s_duty_cycle = 600; answer = 360; actual = control_pilot_get_current(); TEST_ASSERT_EQUAL(answer, actual); // more cases... }
Parameterized tests give us a way to perform this kind of repetitive yet thorough test concisely.
We can rewrite the above test like this:
TEST_CASE(10, 0) TEST_CASE(97, 60) TEST_CASE(600, 360) // ... more TEST_CASE calls void test_various_duty_cycles(uint32_t duty_cycle, uint16_t current) { s_duty_cycle = duty_cycle; TEST_ASSERT_EQUAL(current, control_pilot_get_current()); }
Parameterized tests are a great way to test your functions for a lot of edge cases at once. Each test case will run as a separate test; setup_test
will run before each test case and teardown_test
will run after each test case, and each case will print out on a separate line. You’ll get output that looks like this:
projects/charger/test/test_control_pilot.c:22:test_various_duty_cycles(10, 0):PASS projects/charger/test/test_control_pilot.c:22:test_various_duty_cycles(97, 60):PASS projects/charger/test/test_control_pilot.c:22:test_various_duty_cycles(600, 360):PASS ...
Warning: only integer literals, symbols defined outside the test file, and derived expressions are currently supported in TEST_CASE calls. TEST_CASE calls also can’t be split over multiple lines. So:
// OK TEST_CASE(0x1234) // OK - test logs will have "test_function(GPIO_PORT_A)" #include "gpio.h" TEST_CASE(GPIO_PORT_A) // OK - test logs will have "test_function((GpioAddress)CONTROLLER_BOARD_ADDR_LED_BLUE)" #include "controller_board_pins.h" TEST_CASE((GpioAddress)CONTROLLER_BOARD_ADDR_LED_BLUE) // OK - test logs will have "test_function(10*50/(5+5))", not "test_function(50)" TEST_CASE(10*50/(5+5)) // Not OK - FOO is defined in the current file #define FOO 0x1234 TEST_CASE(FOO) // Not OK - strings aren't allowed TEST_CASE("foo") // Not OK - arrays aren't allowed TEST_CASE(uint8_t[]{1, 2, 3}) // Not OK - Foo is defined in the current file, and no compound literals typedef struct Foo { uint8_t bar; } Foo; TEST_CASE((Foo){ .bar = 2 }) // Not OK - compound literals aren't allowed TEST_CASE((GpioAddress){ .port = GPIO_PORT_A, .pin = 4 }) // Not OK - multiple lines TEST_CASE( 1, 2, 3 )
This is because our old version of the Unity test framework (http://www.throwtheswitch.org/unity) literally copies the arguments you put in the TEST_CASE
call to the generated test runner file, which runs your tests. It doesn’t have access to any of the symbols defined in your test file, but it does include everything your test file does. It’s also really dumb, so it does silly things like not recognizing multiple line TEST_CASE
calls, and interpreting }
as marking a new line.
A lot of this weirdness will be fixed when we update Unity: https://uwmidsun.atlassian.net/browse/SOFT-420
Aside: the TEST_CASE
macro is defined in test_helpers.h, so you’ll have to include it.