Hello all,
More unit testing excitement! I think there are some valuable changes
we could make to the `test/testutil` API, so I wanted to share some
ideas with the community. I introduced many of the issues that I want
to focus on, so I don't really feel obligated to be polite here :). I
just wanted to give that disclaimer in case it feels like I'm being
overly harsh.
I already touched on one of testutil's problems in an earlier email: a
lack of support for standalone self tests. Recall that a self test is
something that runs in the simulator when you type `newt test
<test-name>`. In my earlier email, I expressed that these tests should
automatically execute `sysinit()` before running.
Now I want to discuss a second problem that I see with this library: an
abundance of configurable callbacks that makes the API too complicated.
I think we can remove about half of the callbacks without losing any
functionality. The execution flow of a test suite is described in a
comment in `testutil.h`:
TEST_SUITE
tu_suite_init -> ts_suite_init_cb
tu_suite_pre_test -> ts_case_pre_test_cb
tu_case_init -> tc_case_init_cb
tu_case_pre_test -> tc_case_pre_test_cb
TEST_CASE
tu_case_post_test -> tc_case_post_test_cb
tu_case_pass/tu_case_fail -> ts_case_{pass,fail}_cb
tu_case_complete
tu_suite_post_test -> ts_case_post_test_cb
tu_suite_complete -> ts_suite_complete_cb
That is a lot of callbacks for a suite with one test case! Let's go
through them one by one.
tu_suite_init:
Description: Gets called once per suite, before the suite runs.
Proposal: Remove.
Rationale: If a suite needs to do any setup, it can do so directly
at the top of its block.
tu_suite_pre_test:
Description: Gets called at the start of each case in the suite.
Proposal: Keep.
Rationale: This callback is the very reason why we group test cases
into suites. Similar test cases need to start from the
same state.
tu_case_init:
Description: Same as tu_suite_pre_test.
Proposal: Remove.
Rationale: Redundant.
tu_case_pre_test:
Description: Gets called once at the start of the next test case.
Proposal: Remove.
Rationale: A test case is just a block of C code. If it needs to
perform some setup, it can do it directly at the start of
the test.
tu_case_post_test:
Description: Gets called at the end of the current / next test case.
Proposal: Keep.
Rationale: A test case may terminate prematurely due to a failed
`TEST_ASSERT_FATAL()`. We need a callback to perform
clean up because the test case may not reach the end of
its block.
tu_case_pass / tu_case_fail:
Description: One of these gets called whenever a test case
completes.
Proposal: Keep.
Rationale: We need some way to report results.
tu_suite_post_test:
Description: Gets called at the end of each case in the suite.
Proposal: Remove.
Rationale: Test cases can already clean up individually using
`tu_case_post_test`. I can see how common clean up code
might be useful, but I don't think this is needed often
enough to justify the added API complexity. For
reference, this function is only used in one place
(NFFS), and it is used unnecessarily - the calls can be
removed with no effect on the tests.
tu_suite_complete:
Description: Gets called at the end of each test suite.
Proposal: Remove.
Rationale: If a test suite needs to perform any cleanup (unlikely),
it can do so directly at the end of its code block.
Unlike a test case, a test suite never terminates early,
so the code at the end of a suite always gets executed.
If we make the proposed removals, the execution flow looks like this:
TEST_SUITE
TEST_CASE
tu_suite_pre_test_cb
<test-body>
tu_case_pass/tu_case_fail
tu_case_post_test_cb
I think that looks better!
I also wanted to talk about how I think these callbacks should get
configured:
tu_case_pass / tu_case_fail:
Configured at the start of the application.
tu_suite_pre_test_cb:
Always configured at the top of a test suite body.
tu_case_post_test_cb:
Always configured at the top of a test case body.
Finally, I've added an example test suite at the bottom of this
email. This is a totally ridiculous set of tests that verify the
machine's (or compiler's) ability to do arithmetic. Ridiculous, but it
is just intended to demonstrate the proposed API changes.
I plan to submit a PR with the proposed changes soon. So feel free to
comment here, or wait for the PR. In any case, all comments are
welcome.
Thanks,
Chris
------
TEST_SUITE(arithmetic_suite)
{
/* Configure a "setup" function that runs at the start of every
* case in this suite.
*/
tu_suite_set_pre_test_cb(arithmetic_pre_test, NULL);
/* Run each test case in sequence. */
test_case_add();
test_case_div();
/* If suite needs to clean up (unlikely), it does it here
* directly.
*/
}
TEST_CASE_SELF(test_case_add)
{
/* Configure a cleanup function for this test case. */
tu_case_set_post_test_cb(adder_uninit, NULL);
/* Initialize the "adder". */
adder_init();
/* Verify adder behavior. */
TEST_ASSERT(0 + 0 == 0);
TEST_ASSERT(100 + 20 == 120);
TEST_ASSERT(UINT_MAX + 1 == 0);
}
/**
* Helper function for division test. Performs a single integer
* division and checks the result.
*/
static void
test_div_int(int num, int den, int expected)
{
int answer;
TEST_ASSERT_FATAL(den != 0, "bug in test: 0 denominator");
answer = num / den;
TEST_ASSERT(answer == expected,
"%d / %d, have=%d want=%d",
num, den, answer, expected);
}
TEST_CASE_SELF(test_case_div)
{
static const struct {
int num;
int den;
int expected;
} table[] = {
{ 0, 100, 0 },
{ 1, 2, 0 },
{ 53, 8, 6 },
};
int i;
for (i = 0; i < sizeof table / sizeof table[0]; i++) {
test_div_int(table[i].num, table[i].den, table[i].expected);
}
}