Scuttle
Easy unit testing for C projects
v1.0.1
MIT License, © 2020 Scott Madin smadin@gmail.com
Overview
Scuttle ("Simple C Unit Testing Tool") is a small unit-testing facility written in C. Scuttle consists of a header file, scuttle.h, which defines the macros you use to write your tests, and a Bash script, scuttle.sh, which parses your test suites and generates the scaffolding to run them.
Installation
To install Scuttle systemwide, extract the archive, and from the root of the Scuttle directory run sudo make install. This will install scuttle.h in /usr/local/include/ and scuttle.sh in /usr/local/lib/scuttle/.
Scuttle does not need to be installed systemwide, however: you can incorporate it into your project directly (for example if you do not have root access). Simply copy include/scuttle.h and src/scuttle.sh into appropriate locations in your project directory.
Usage
Requirements and assumptions
Scuttle assumes what I believe to be a fairly standard structure for small to medium-sized C projects:
- the entire project contained in an appropriately-named directory (
<projname>) - source files in
<projname>/src/ - headers in
<projname>/include/ - a Makefile or other build system that places compiled object files in
<projname>/obj/and executable binaries in<projname>/bin/
Scuttle depends on Bash and GNU Make (you need not use Make to build your main project, but Scuttle will use it to build the test harness), and a modern C compiler. (The Makefile Scuttle generates will try to use clang or gcc, then fall back to the default. The compiler must support the flags -g -Wall -Wextra -Werror -std=c99.)
From here on, assume all directory names are relative to the project root <projname>/.
Adding Scuttle to your build
These instructions assume your project does use GNU Make to build, so adjust accordingly if you use a different build tool.
Add a test target to your main project Makefile, and call the scuttle.sh script:
.PHONY: test
test:
bash /usr/local/lib/scuttle/scuttle.sh <testdir>
$(MAKE) -C <testdir> test
cat <testdir>/log/test_<projname>.logWhere <testdir> is the subdirectory in which you will place your test suites. If you use the default name test, you can omit the <testdir> parameter when calling scuttle.sh.
Scuttle will look in <testdir> for source files named test_<module>.c, where <module> is the name of a module of the main project (assumed to exist as an object file obj/<module>.o) to which the test suite corresponds. It will parse those test suites, and generate a header and a data file for each one, as well as a main test harness program and a Makefile in <testdir>.
Writing Scuttle test suites
Now it's time to write some tests, using the macros scuttle.h defines.
As a very simple example, suppose your project has the module foo, defined in include/foo.h:
#ifndef _FOO_H
#define _FOO_H
/* foo() should return 42 */
int foo();
/* foostr() should return 1 (success) and write "foo" into buf, or return 0 (failure) if buf is NULL or too small */
int foostr(char *buf, size_t bufsz);
#endif /* _FOO_H */Which is compiled to obj/foo.o. To test these functions, write a suite <testdir>/test_foo.c:
#include "foo.h"
#include "test_foo.h" /* test_foo.h will be generated by scuttle.sh */
SSUITE_INIT(<projname>_foo)
/* code here will run once, before any tests in the suite */
SSUITE_READY
STEST_SETUP
/* code here will run before each test */
STEST_SETUP_END
STEST_TEARDOWN
/* code here will run after each test */
STEST_TEARDOWN_END
STEST_START(foo_returns_fortytwo)
int i = foo();
SASSERT(i == 42) /* simple assertion */
SREFUTE(i != 42) /* negative assertion */
SASSERT_EQ(i, 42) /* equality assertion */
STEST_END
STEST_START(foostr_sets_buf)
char buf[10];
int i = foostr(buf, 10);
SASSERT(i) /* did foostr() return 1 (success)? */
const char *expected = "foo";
SASSERT_STREQ(buf, expected) /* did foostr() put "foo" in buf? */
STEST_END
STEST_START(foostr_doesnt_set_buf)
char buf[2];
int i = foostr(buf, 2);
SREFUTE(i) /* did foostr() return 0 (failure)? */
i = foostr(NULL, 10);
SREFUTE(i) /* did foostr() return 0 (failure)? */
STEST_ENDNow simply run your make test target from your main project directory, and assuming the foo module does work correctly, you'll see some build output followed by the contents of <testdir>/log/test_<projname>.log:
Test suite <projname>_foo:
*** Suite passed: 3 / 3 tests passed.
*** 1 / 1 suites passed.
See the example/ subdirectory of the Scuttle archive for a complete, very simple, sample project including this foo module.
Reference
Suite macros
The suite definition macros are required in each Scuttle test suite, even if you don't need any special suite initialization or test setup code.
SSUITE_INIT(<projname>_<suitename>): Begins the suite initialization function, and declares the name of the test suite.<projname>must match the name of the project's root directory, and<suitename>must match the filenametest_<suitename>.c. Code betweenSSUITE_INIT()andSSUITE_READYis run once, just before any tests in the suite are run.SSUITE_READY: Ends the suite initialization function.STEST_SETUP: Begins the per-test setup function. Code betweenSTEST_SETUPandSTEST_SETUP_ENDis run just before each test in the suite is run.STEST_SETUP_END: Ends the per-test setup function.STEST_TEARDOWN: Begins the per-test teardown function. Code betweenSTEST_TEARDOWNandSTEST_TEARDOWN_ENDis run just after each test in the suite is run.STEST_TEARDOWN_END: Ends the per-test teardown function.
Test macros
Every Scuttle test suite must define at least one test.
STEST_START(<testname>): Begins the test case. Scuttle will extract<testname>when generating the test scaffolding. BetweenSTEST_START()andSTEST_ENDis where you write your test code.STEST_END: Ends the test case.
Assertion macros
Use Scuttle's assertion macros in your tests, to check the output of the functions you're testing and raise errors if something isn't right. You can put more than one assertion in a test case, but any assertion failure will bail out of that case. (If a test case fails, the rest of the cases in the suite will still be run.)
SASSERT(x): asserts that the expression represented byxis true (non-zero/non-NULL).SREFUTE(x): asserts that the expression is false (zero/NULL).SASSERT_NULL(x): asserts specifically that the expression isNULL.SREFUTE_NULL(x): asserts that the expression is notNULL.SASSERT_EQ(x, y): asserts thatxandyare equal at the level of variable equality (have the same value, are the same pointer, etc.) — note this is different from string equality.SREFUTE_EQ(x, y): asserts thatxandyare not equal.SASSERT_STREQ(x, y): asserts thatxandyare either bothNULL, or point to the same string, or are pointers to equal (strncmp()returns 0) strings. BecauseSASSERT_STREQ()performs pointer comparison, do not pass a string literal or literalNULLto it; instead you must instantiate achar *variable in your test to hold that value.SREFUTE_STREQ(x, y): asserts thatxandyare not equal strings.