Mini-unit 2: unit testing
Learning goals
You will learn the following concepts/skills:
- unit testing – systematically test your code
- framework – create a simple framework for unit-testing future C code
- organization – learn how to organize a test suite
- test coverage – measure how thoroughly your tests exercise your code
- C preprocessor − practice using
#define
symbols and macros with#ifdef
.
Overview of the Miniunit series (HW06, HW07, HW08)
This assignment is part 2 of a 3-part series collectively referred to as miniunit.
Real-world software projects typically comprise tens, hundreds, or many thousands of files. Even in your ECE courses, your programming assignments will be getting bigger and more complex. Testing code by hand (i.e., by playing with it) is useless since there are so many components, and ways things can go wrong. Hand-testing effectively would be complex—and prohibitively slow.
Most serious software projects use two tools: a unit testing framework
(e.g., CUnit)
to organize and coordinate tests, and a build system
(e.g., make)
to manage the process of compiling, testing, and deploying the project. In this homework,
you will get a light introduction to both. In addition, you will learn to use console
logging effectively (as a complement to gdb), without littering your code with
printf(…)
statements.
In the Miniunit Series, you will create the following:
-
clog.h – your own library for smarter
printf
-style debugging and logging. From now on, instead of debugging withprintf(…)
directly, you will use function-like macros to print values in various formats and colors to make the output easier to view. You will use preprocessor directives to ensure that your debugging code does not interfere with your tests, or show up inadvertently when others are testing your code. - Makefile – input file for the make build system. You will be able to build, test, submit, and/or pre-test your code with one command from bash or directly from Vim (and other editors).
-
miniunit.h – your own simple unit test library. You
can use this to test future assignments in this class, or for any other project you do
in C (or C++) beyond ECE 264. This consists of four
#define
macros that you can use in your test code. The most important macro you will create ismu_check(…)
, which is somewhat similar toassert(…)
.
About miniunit.h
Until now, we have used a very low-tech method of structuring our tests
with the expected.txt and the diff
command. That method
is based on the more general principle of unit testing.
In unit testing, programmers write collections of small functions that test
some aspect of a program. For example, in mintf(…)
in
HW05,
you might have a unit test for small positive integers in base 10, another unit
for small negative numbers, one for extreme bases, extreme positive values,
extreme negative values, and so on. Each test is its own function.
As the number and complexity of tests grows, it becomes necessary to have
a foundation of code for running and organizing them. This is called a
unit test framework. There are
unit test frameworks for every major programming language.
For C, one example is
CUnit.
These typically have special-purpose macros for checking assumptions, similarly
to the assert(…)
macro, which we have already
covered this semester, but with a unit test framework, we may allow the code
to continue, even if an assumption is not met, since the goal is simply to check.
In addition to the checking macros, real-world testing frameworks have mechanisms
for setting up the needed environment that the program-in-test
depends on (e.g., files, network connections, etc.), and sophisticated interfaces
for visualizing the results and scheduling test runs.
In this homework, you will create a very simple unit test framework… let's call it
a unit test library. You may use this to test future assignments. We will illustrate
by example.
Using miniunit.h to test — minimal example
Suppose you have the following (very trivial) module for doing arithmetic calculations involving the number 3.
three.c 1 #include "three.h" 2 3 int times_3(int n) { 4 return n * 3; 5 } 6 7 int divided_by_3(int n) { 8 return n % 3; // BUG 9 } 10 11 int plus_3(int n) { 12 return n * 3; // BUG 13 } 14 15 int minus_3(int n) { 16 return n - 3; 17 }
three.h 1 int times_3(int n); 2 int divided_by_3(int n); 3 int plus_3(int n); 4 int minus_3(int n);
Here is the general format for every unit test function (e.g., in test_three.c). You will typically have several of these in your test_▒▒▒.c.
int test_▒▒▒() { mu_start(); // set up the test // … mu_check(condition); // … mu_check(condition); // … mu_end(); // finish the test }
Then, to call all of your unit test functions, you will have a runner
function (e.g., main(…)
) like this:
int main(int argc, char* argv[]) { mu_run(test_▒▒▒); mu_run(test_▒▒▒); mu_run(test_▒▒▒); mu_run(test_▒▒▒); return EXIT_SUCCESS; }
Using the above format, we create the following unit test suite (group of tests) for our three-arithmetic module.
// test_three.c
#include <stdio.h>
#include <stdlib.h>
#include "miniunit.h"
#include "three.h"
int test_plus_3() {
mu_start();
mu_check(plus_3(1) == 4); // will FAIL
mu_end();
}
int test_minus_3() {
mu_start();
mu_check(minus_3(1) == -2);
mu_end();
}
int test_times_3() {
mu_start();
mu_check(times_3(1) == 3);
mu_end();
}
int test_divided_by_3() {
mu_start();
mu_check(divided_by_3(1) == 0); // will FAIL
mu_end();
}
int main(int argc, char* argv[]) {
mu_run(test_plus_3); // will FAIL
mu_run(test_minus_3);
mu_run(test_times_3);
mu_run(test_divided_by_3); // will FAIL
return EXIT_SUCCESS;
}
Your unit test library, using #define
macros will convert that seemingly
simple test code into a colored summary, including the name of the test function
and the line where it failed. Here is the expected out put for the test above:
$ gcc -o test_three three.c test_three.c $ ./test_three Test failed: test_plus_3 at line 8 Test passed: test_minus_3 Test passed: test_times_3 Test failed: test_divided_by_3 at line 26 $
1. Set up your homework directory.
There is no starter code, though you will use your code from HW06.
Create a directory for HW07.
you@ecegrid-thin1 ~/
$
cd 264
you@ecegrid-thin1 ~/264
$
mkdir hw07
you@ecegrid-thin1 ~/264
$
cd hw07
you@ecegrid-thin1 ~/264/hw07
$
Copy your HW06 code into your HW07 directory.
you@ecegrid-thin1 ~/264/hw07
$
cp ../hw06/* ./
you@ecegrid-thin1 ~/264/hw07
$
The most important file you will create in HW07 is miniunit.h. You will create that from scratch (i.e., starting with a blank file).
2. Create your miniunit.h.
Create a file called miniunit.h from scratch.
you@ecegrid-thin1 ~/264/hw07
$
vim miniunit.h
Create the five #define
macros as specified in the Requirements table.
⚠ Success and failure messages should be printed using
logf_green(…)
and
logf_red(…)
.
⚠ Success and failure messages should be printed by
mu_run(…)
(not mu_check(…)
),
and should not rely on logf_green(…)
and
logf_red(…)
.
Those will be disabled when DEBUG
is not defined (i.e., when
-DDEBUG
was not passed to the compiler); miniunit.h
is expected to work either way.
3. Use miniunit.h to improve test_count_words.c
This part will teach you how to use miniunit.h in practice for future assignments. We are giving you the code for free, so it shouldn't take more than about 15 minutes to type and test.
- At the top of test_count_words.c, include miniunit.h.
(You can also remove the
#include "clog.h"
since your miniunit.h will include that.) - Add three test functions above
main(…)
:int _test_empty()
,int _test_simple()
, andint _test_hard()
. - Inside each test function, add
mu_start()
at the beginning, andmu_end(…)
just before the end. - Add at least one call to
mu_check(…)
in betweenmu_start(…)
andmu_end(…)
. - At the top of
main(…)
, addmu_run(_test_empty)
,mu_run(_test_simple)
, andmu_run(_test_hard)
. - The result may look something like this:
#include <stdio.h> #include <stdlib.h> #include "count_words.h" #include "miniunit.h" int _test_empty() { mu_start(); //------------------------------- mu_check(count_words("") == 0); //------------------------------- mu_end(); } int _test_simple() { mu_start(); //------------------------------- mu_check(count_words("apple") == 1); mu_check(count_words("boring boxes") == 2); mu_check(count_words("apple banana") == 2); mu_check(count_words("apple banana cherry") == 3); //------------------------------- mu_end(); } int _test_hard() { mu_start(); //------------------------------- mu_check(count_words("famigerate fiddle-faddle") == 2); mu_check(count_words("Mary's mongoose") == 2); mu_check(count_words("plumbers' platitudes") == 2); //------------------------------- mu_end(); } int main(int argc, char* argv[]) { mu_run(_test_empty); mu_run(_test_simple); mu_run(_test_hard); log_int(count_words("My apples are sweet.")); log_int(count_words("My friend's apples are sweeter.")); log_int(count_words("A pear is sweeter than an apple..")); return EXIT_SUCCESS; }
Note: Due to the intentially planted bugs in count_words.c,_test_simple(…)
will fail on "apple banana" (due to 'a'), and_test_hard(…)
will fail on "familgerate fiddle-faddle" (due to '-'). - You must submit your test_count_words.c, even if it is identical to the code above.
count_words(…)
has intentionally planted bugs.
It is okay that some of the results are obviously incorrect and the tests fail. The
purpose of tests is to detect flaws. If the flaws are detected, then the test worked
correctly.
How much work is this?
Requirements
- Your submission must contain each of the following files, as specified:
file contents clog.h (same as HW06)miniunit.h macros mu start()
- Declare a local variable and initialize it to 0.
- The purpose of this variable is to store the line
number of the first "check".
- By “check” we mean a call to
mu_check(…)
in the test code that uses miniunit.h.
- By “check” we mean a call to
- ⚠ Name of variable must begin with
__mu_
(e.g., __mu_line_number_of_first_failure). - Initial value of this variable should be 0.
- Hint:
mu_start(…)
will be only one line; there should be no semicolon.
mu check(condition)
- If condition is false, store the line number of this
mu_check(…)
call in the variable that you created inmu_start(…)
—but only for the first call tomu_check(…)
with a condition that is false.- Store the line number only on the first check that fails.
- If you have multiple calls to
mu_check(…)
that fail, you want to keep only the line number of the first one that failed.
- You can get the current line number with
__LINE__
.
mu run(function)
- Call
function()
(with no parameters). - If it returns 0, then print (in green on stdout):
Test passed: function - If it returns a line number (≥1), then print (in red on stdout):
Test failed: function at line line# - ⚠ This should work even when the program is not compiled with
-DDEBUG
.- This may require minor changes to your clog.h to ensure that
mu_run(…)
works with or without-DDEBUG
, but thelog▒▒▒(…)
macros only work when compiled with-DDEBUG
. - Hint: This means
mu_run(…)
should not calllogf_green(…)
orlogf_red(…)
directly. This is a big hint! The snippet given in the Q&A of HW06 should help with this.
- This may require minor changes to your clog.h to ensure that
mu end()
mu_check(…)
) that failed, or 0 if all checks succeeded.- Hint: This will be just one line; there should be no semicolon.
mu check strings equal(s1, s2)
mu_check(…)
andstrcmp(…)
to check that two strings (s1 and s2) are equal.- Hint: This will be just one line; there should be no semicolon.
- This is just a wrapper for
mu_check(…) to make it easier to compare strings.
mu_check_strings_equal("A", "A")
should have the same result asmu_check(5 == 5)
test_count_words.c (as described in part 3 above) - You may hand-copy any code snippets you find in this homework
description into your HW07 submission.
- Do not use copy-paste. You learn more from hand-copying unfamiliar syntax. Expect problems if you ignore this.
- Adaptation is strongly recommended. Some snippets may not work in your file as is.
- Be sure you understand what you are copying. Correct functioning of your code is your responsibility.
- Copying from this page is not necessary. This permission is given as a convenience, since some of the syntax may be unfamiliar, and this homework is more tightly specified than most others.
- Names of helper macros (if any) must begin with “
__mu_
”. - Do not repeat the ANSI codes anywhere other than clog.h.
- Use
logf_green(…)
andlogf_red(…)
from your clog.h to print messages of success and failure of tests. - Required macros in clog.h
(i.e.,
log▒▒▒(…)
) should work ONLY when the symbol DEBUG is defined (i.e., when you compile withgcc -DDEBUG
).- Hint: You will need to use some combination of
#ifdef
,#ifndef
,#else
, and#define
- If you use any helper macros (e.g.,
__mu_log_color(…)
), they may work even when DEBUG is not defined.
- Hint: You will need to use some combination of
- ⚠ Macros in miniunit.h should work regardless of the symbols defined (e.g., DEBUG).
-
You may use any of the following:
header functions/symbols allowed in… stdbool.h bool
,true
,false
*.c
,*.h
stdio.h printf
,fputs
,fprintf
,stdout
*.c
,*.h
string.h strcmp
test_count_words.c
,miniunit.h
unistd.h isatty
,STDOUT_FILENO
*.c
,*.h
stdlib.h EXIT_SUCCESS
test_count_words.c
,test_clog.c
- Code that includes clog.h and/or miniunit.h and uses macros from them must compile and run whether or not NDEBUG was defined.
- miniunit.h should have
#include "clog.h"
so that users of miniunit.h don't need to include both. -
Submissions must meet the code quality standards
and the course policies on homework and academic integrity.
-
That means everything must compile successfully, even when compiled
with the usual compiler flags
(
gcc -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic
)—and without-D NDEBUG
. Furthermore, tests using your miniunit.h should work properly with or without-D NDEBUG
.
-
That means everything must compile successfully, even when compiled
with the usual compiler flags
(
- Write multi-line macros with one C statement on each line. Do not try to cram many statements on a single line of code. That would not be readable.
- Indent your macros similarly to regular C code. Your code must be readable.
Submit
To submit HW07 from within your hw07 directory,
type
264submit HW07 miniunit.h clog.h test_count_words.c
Pre-tester ●
The pre-tester for HW07 has not yet been released. As soon as it is ready, this note will be changed.
Q&A
Should I have a semicolon at the end of the RHS of a
No. The person using the macro will normally include the semicolon.#define
macroHow can I continue the RHS of a
Put a backslash ("\") at the end of the line.#define
macro definition onto the next line#define profess_love_for_food(food) printf("I love %s", \ food)
Can I have a macro with multiple C statements
Yes. In theory, you could just have the two statements separated by a semicolon like this:// BAD #define profess_love_for_two_foods(food1, food2) \ printf("I love %s", food1); \ printf("I love %s", food2)
However, that would lead to surprising results if someone who doesn't follow the code quality standards calls that macro in an if statement like this:if(1 == 0) profess_love_for_two_foods("soap", "poison");
Only the first statment would be connected to theif
statement.if(1 == 0) printf("I love %s", "soap"); printf("I love %s", "poison");
It is tempting to just put curly braces around the two statements, but that also causes problems.// BAD #define profess_love_for_two_foods(food1, food2) { \ printf("I love %s", food1); \ printf("I love %s", food2); \ }
The problem comes back to uncivilized oafs who write
if
statements without curly braces, like this:if(age >= 30) profess_love_for_two_foods("chocolate", "pizza"); else profess_love_for_two_foods("spinach", "broccoli");
The above example would result in this:
if(age >= 30) { printf("I love %s", "chocolate"); printf("I love %s", "pizza"); }; ← PROBLEM else { printf("I love %s", "spinach"); printf("I love %s", "broccoli"); };
The standard solution is to wrap the statements in ado { … } while(false)
block. Because thedo…while
requires a semicolon, this actually works out like we want.Yes, it is ugly. Hacks like this are not something the instructor would normally condone, but it is standard practice because there are very few truly versatile options for this.// USE THIS WAY #define profess_love_for_two_foods(food1, food2) do { \ printf("I love %s", food1); \ printf("I love %s", food2); \ } while(false)
Why does miniunit.h need
These give a relatively clean and consistent form to your unit tests, so you can focus on the code that matters for each particular test.mu_start(…)
andmu_end(…)
Withoutmu_start(…)
andmu_end(…)
, one might resort to a naïve approach, like this:YUK!int test_count_words_▒▒▒▒▒() { int test__mu_failure_line_num = __MU_SUCCESS; mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); return __mu_failure_line_num; }The programmer should not have to know the specific internal names used by miniunit.h! If they get it wrong, then it won't work properly withmu_check(…)
.Of course, they could avoid the problem of consistency if they do the whole thing without any macros—i.e., without miniunit.h—but then the above skeleton would be even messier!YUK! YUK! YUK! YUK! YUK!int test_count_words_▒▒▒▒▒() { const int __MU_SUCCESS = 0; int __mu_failure_line_num = __MU_SUCCESS; if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } return __mu_failure_line_num; }Withmu_start(…)
andmu_end(…)
—and adding a divider comment (optional)—you get this:GOODint test_count_words_▒▒▒▒▒() { mu_start(); //──────────────────────────────────────── mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); //──────────────────────────────────────── mu_end(); }mu_start(…)
andmu_end(…)
also make your unit testing library more extensible. If you wanted to change how your tests are organized and/or reported, you could do so without changing all of your test code that uses it.What does
It expands to the text of the expression, instead of its value. This is easiest to see if you test using the /usr/bin/cpp command.#x
do in a#define
macroHere is an example, which uses thelog_int(…)
snippet given in the Requirements table.// demonstrate_hash.c #include <stdio.h> #include <stdlib.h> #define log_int(n) fprintf({{ stream }}, "%s == %d\n", (#n), (n)) int main(int argc, char* argv[]) { log_int(3 + 3); return EXIT_SUCCESS; }
If we process that with the preprocessor directly (instead of via gcc), we can see what it becomes.you@ecegrid-thin1 ~/HW07 $
/usr/bin/cpp demonstrate_hash.c -DDEBUG | indent -kr
… int main(int argc, char* argv[]) { fprintf({{ stream }}, "%s == %d\n", ("3 + 3"), (3 + 3)); return 0; }Notice that the third argument tofprintf(…)
is a string literal,"3 + 3"
—the text of the argument that was passed tolog_int(…)
. That is different from the fourth argument, which is the value of that parameter,3 + 3
(= 6).How do I make
Define a helper macro for printing in color outside themu_run(…)
print the success/failure messages in color even when the DEBUG symbols is not defined (i.e.,-DDEBUG
not passed to GCC), and without duplicating the ANSI codes in miniunit.h#ifdef/#endif
. You can use the one in the Q&A of HW06 or make your own. You will need to define the ANSI codes (#define ANSI_RED …
, etc.) outside the#ifdef/#endif
, as well.GCC: “ISO C99 requires rest arguments to be used.” ⋯??
In variadic macros, the...
can stand for 1 or more arguments. If yourlogf_red(…)
looks like#define logf_red(format, ...) ▒▒▒▒▒▒▒▒▒▒
, it will likely work as long as you pass ≥2 arguments (e.g.,logf_red("I like %d", 5)
), but if you pass only a string (e.g.,logf_red("orange sky")
), then there are 0 arguments for the...
. It needs ≥1. The solution for our purposes is to remove the first argument (format
). The correct form was given in the Q&A of HW06.GCC: “error: ‘true’ undeclared” ⋯???
The
GCC: “error: ‘false’ undeclared” ⋯??true
andfalse
constants are defined in a standard header called stdbool.h; you need to include it (i.e.,#include <stdbool.h>
) in any file where you use them (e.g., clog.h). This was communicated in #8 of the Requirements table, as well as the Code Quality Standards, which have more aboutbool
,true
, andfalse
.GCC: “implicit declaration of function ‘isatty’” ⋯???
The
GCC “‘STDOUT_FILENO’ undeclared” ⋯??isatty(…)
function and STDOUT_FILENO symbol are defined in a standard header called unistd.h you need to include it (i.e.,#include <unistd.h>
). This was communicated in #7 and #8 of the Requirements table.GCC: “__mu_failure_line_num is not defined” ⋯ Why
If you get that withinmain(…)
, make sure you are not trying to use the variable declared inmu_start(…)
in yourmu_run(…)
. Yourmu_start(…)
is expanded in the context of a_test_▒▒▒(…)
function, whilemu_run(…)
is expanded in the context ofmain(…)
.CPP: "Unexpected EOF (end-of-file)" (or similar). Why?
You probably have unmatched braces somewhere in your clog.h or miniunit.h. First, fix your indents. It will make the errors more obvious. Then, be sure to use/usr/bin/cpp test_count_words.c | indent -kr
or/usr/bin/cpp test_count_words.c -DDEBUG | indent -kr
to make the output readable.How do I start? Can I use TDD?
Try this progression:- Make it work for an empty test function (i.e., no calls to
mu_check(…)
). - Make it work for tests that always pass. In other words, assume the condition is true.
- Make it work for test functions with one test that fails.
- Make it work for test functions with any number of tests that fail.
- Make it work for an empty test function (i.e., no calls to
Updates
2/24/2022 | Clarified use of mu_check_strings_equal(…) |