Atto is the simplest-to-use C unit test framework, in just one header file,
without malloc()
, without fork()
, without dependencies, ready for
embedded systems that can at least call printf()
- and even those who cannot,
can easily adapt it!
Most probably you will understand most about Atto if you just read its
header: atto.h
. I promise it's not that long, if you exclude
the documentation.
The goal of a unit test is generally very simple: verify that the obtained value equals to the expected one. More generally if a boolean expression is true (e.g. a value is in the allowed range).
There are so many complex unit test frameworks to do this in C and C++
which just increases complexity of the whole project. Suppose you are just
writing a simple program and want to test it. Oh, boy! Start managing the
dependencies for the framework, check if you can install it, maybe you need
Docker etc. Some frameworks even require fork()
- how could that work on
an embedded system?
Atto is born to remove all of that complexity. Heavily inspired by MinUnit, which has just 3 lines of code, Atto is just a header file you can include statically (copy-paste) in your project and start writing your tests. Atto is so tiny that even its name means "tiny" ;)
The output is basic, but enough: indicates just where the test case failed, by filename and line number. Open that file, go to that line. There is the error. Start using the debugger around that point.
Passing tests are not printed, to avoid cluttering the output.
You can still explicitly request a status report at any point in the
test suite codebase (e.g. after a cluster of testcases of similar nature)
with atto_report()
.
Only the C standard library!
stdio.h
, forprintf()
- if your system does not have it, replace the one call ofprintf()
inatto.h
with something else!math.h
, forfabs()
,fabsf()
,isnan()
,isinf()
,isfinite()
string.h
, forstrncmp()
,memcmp()
stddef.h
forsize_t
No malloc()
or fork()
required
Use the atto_*
macros in your test cases.
Here is an example where we test the sqrt()
function from math.h
:
#include <math.h>
#include "atto.h"
static void test_sqrt_valid_values(void)
{
atto_eq(0.0, sqrt(0.0));
atto_eq(1.0, sqrt(1.0));
atto_eq(2.0, sqrt(4.0));
atto_ddelta(1.4142, sqrt(2.0), 0.001);
atto_ddelta(1.7321, sqrt(3.0), 1e-8); // This fails!
// 1.7321 is not accurate enough
// The test case stops here.
atto_ddelta(2.2361, sqrt(5.0), 1e-8); // This is NOT executed!
}
static void test_sqrt_negative_values(void)
{
atto_nan(sqrt(-1.0));
}
int main(void)
{
test_sqrt_valid_values();
test_sqrt_negative_values();
atto_report(); // Print a small one-line status report
// Looks approximately like this
// REPORT | File: /path/to/my/file.c:27 | Test case: main | Passes: 5 | Failures: 1
return atto_at_least_one_fail; // Non-zero in case of error to provide a proper exit-code
}
Check some of my other personal projects, where I use Atto for unit testing!
- Add the files
atto.h
,atto.c
to your project:- You can copy the whole
src
folder into your project. This is probably the fastest and simplest solution. - Alternatively, use
git subtree
to include this repo into your own
- You can copy the whole
- Create a file with your tests, say
test.c
. - Add test case functions returning
void
totest.c
, as the twotest_sqrt_*
functions in the example above. - In each test case call
atto_assert()
,atto_eq()
,atto_flag()
etc. to verify the values you are testing. On a fail, the test case is terminated early and the lines of code after that are not executed. - Add a
main()
function totest.c
which returnsatto_at_least_one_fail
. By doing so, the exit code of the test executable will be1
in case at least one test failed. Particularly useful when running the test executables in a pipeline that should stop when something is broken. Of course on embedded systems one can useatto_at_least_one_fail
in different ways, as there is no returning frommain
; for example by transmitting it via UART. - Call the test case functions from the
main()
. - Compile
test.c
into an executable and run it. - Check its standard output and the process exit code for failed tests.
- Bonus: add some calls to
atto_report()
wherever you want in the test suite code. At least one call at the very end (before themain
returns) is suggested, but also after a set of similar test cases is a good choice to see where something is making the test suite crash, in case so happens.
The output will contain one or more lines like:
FAIL | File: /path/to/some_project/test.c:182 | Test case: test_valid_input_length
-
Open the file
/path/to/some_project/test.c
-
Go to line 182 (use some keyboard shortcut), which is in the function
test_valid_input_length()
Note: sometimes the
path:linenumber
pattern is identified as clickable by your IDE. Maybe that speeds up your search. -
The assertion on that specific line failed. Now up to you to debug why. The easiest way is to place a debugger breakpoint earlier in the test case.
Once the standard output does not contain any FAIL
lines and the exit code
of the process is 0
, you are good to go!
It may not be the best solution for your scenario - it was born for my personal projects where I needed something simple. You are more than welcome to customize it to your needs within your project or simply to use other frameworks.