Summary
The lab assumes that you are familiar with compiling and linking C code and
using make
and Makefiles.
The lab covers
-
How to write unit tests for C using
check
-
How to build and run your unit tests
-
How to generate a code coverage report from a run of your unit tests.
You can download the code for this lab to use and study.
For more information on check
please see the online
documentation. .
How to write unit tests for C using check
For the purposes of this lab we will use the same example as the one found on the
check
tutorial called money.
Unlike the check
tutorial we will only use make
to build and run our code.
We are asked to design a program that can be used to represent amounts in any currency. We should be able to
-
create an amount and its currency, e.g., 25 USD
-
obtain just the amount
-
obtain just the currency
The layout of our folders and directories is as follows
Money ├── Makefile ├── src │ ├── money.c │ └── money.h └── tests └── check_money.c
Here is the contents of money.h
and money.c
#ifndef MONEY_H
#define MONEY_H
typedef struct Money Money;
Money *create_money(int amount, char *currenty);
int money_amount(Money *m);
char *money_currency(Money *m);
void money_free(Money *m);
#endif /* MONEY_H */
#include <stdlib.h>
#include "money.h"
struct Money {
int amount;
char *currency;
};
Money *money_create(int amount, char* currency) {
Money *m = malloc(sizeof(Money));
if (m == NULL) {
return NULL;
}
m->amount = amount;
m->currency = currency;
return m;
}
int money_amount(Money* m) {
return m->amount;
}
char *money_currency(Money* m) {
return m->currency;
}
void money_free(Money* m) {
free(m);
}
check
provides a framework to write and run tests. In order to use the framework we need
to first understand how check
expects our tests to be written and how our tests are
organized into suites.
check
uses the following terms
-
test function is a single function that typically uses
ck_assert_
* functions -
test case is a collection of test functions
-
test suite is a collection of test cases
-
suite runner is a function that takes a test suite and runs all test cases and their test functions
In order to create tests for our code we have to first create test
functions. We create a test function by using the macros START_TEST
and END_TEST
.
For example here is a test function for create_money
START_TEST(test_money_create) { (1)
Money *m;
extern Money *money_create(int, char*);
m = money_create(5, "USD");
ck_assert_int_eq(money_amount(m), 5); (2)
ck_assert_str_eq(money_currency(m), "USD"); (3)
money_free(m);
} END_TEST (4)
1 | You can think of the START_TEST macro as a function that takes
as an argument the name of our test function, in this case
test_money_create . The body of our test function follows the same rules
as the body of a C function. |
2 | ck_assert_int_eq is a function provided by check that we can use to assert
equality between C int s. To see all the available assert functions provided by check see the online documentation. |
3 | ck_assert_str_eq is a function provided by check that we can use to assert
equality between C string s. To see all the available assert functions provided by check see the online documentation. |
4 | Finally, we must always finish our test function’s definition with END_TEST . |
So once we have written our test functions we need to add them to test cases. Once we have created our test cases we then have to add them to a suite.
Suite *money_suite(void) { (1)
Suite *s; (2)
TCase *tc_core; (3)
s = suite_create("Money"); (4)
tc_core = tcase_create("Core");(5)
tcase_add_test(tc_core, test_money_create); (6)
suite_add_tcase(s, tc_core); (7)
return s;
}
1 | The C function money_suite takes not arguments and returns a pointer to a Suite . |
2 | Declare s as a pointer to Suite |
3 | Declare tc_core as a pointer to TCase . |
4 | Create a new suite using the function suite_create and give the suite the name Money |
5 | Create a new test case using the function tcase_create and give the test case the name Core |
6 | Add the test function test_money_create to the test case tc_core using the function
tcase_add_test . |
7 | Add the test case tc_core to the suite s using the function suite_add_tcase |
Once we have the preceding two fragments of code that create a test
function, a test case and a test suite we can put everything together
in our main
function.
Here is the complete source file for check_money.c
. The complete
file contains the test function test_money_create
as well as
money_suite
.
#include <check.h> (1)
#include "../src/money.h" (2)
#include <stdlib.h>
START_TEST(test_money_create) {
Money *m;
extern Money *money_create(int, char*);
m = money_create(5, "USD");
ck_assert_int_eq(money_amount(m), 5);
ck_assert_str_eq(money_currency(m), "USD");
money_free(m);
} END_TEST
Suite *money_suite(void) {
Suite *s;
TCase *tc_core;
s = suite_create("Money");
tc_core = tcase_create("Core");
tcase_add_test(tc_core, test_money_create);
suite_add_tcase(s, tc_core);
return s;
}
int main(void) {
int no_failed = 0; (3)
Suite *s; (4)
SRunner *runner; (5)
s = money_suite(); (6)
runner = srunner_create(s); (7)
srunner_run_all(runner, CK_NORMAL); (8)
no_failed = srunner_ntests_failed(runner); (9)
srunner_free(runner); (10)
return (no_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; (11)
}
1 | In order to use the check library we need to include it’s header file |
2 | We include the header file for the C code that we intent to test |
3 | In our main we declare an int variable no_failed to store the number of
failed tests. |
4 | We declare, but not create, s as a pointer to a Suite |
5 | We decalre runner as a pointer to a Suite Runner SRunner . |
6 | We call our function money_suite in order to create and return our test suite. |
7 | We then use our newly created test suite and pass it to the
function srunner_create in order to obtain back a pointer to a
test suite runner. |
8 | We then use the function srunner_run_all and pass our runner and the output mode (CK_NORMAL prints all failed tests) so that check will run our test suite and check our assertions. |
9 | We then call srunner_ntests_failed passing in as argument our
runner in order to extract the number of failed tests. |
10 | We then use srunner_free passing our runner so that we
free any memory that was allocated due to our runner |
11 | finally we successfully return if the number of failed tests is 0 else we fail the test run. |
How to build and run your unit tests
The source code for this lab includes a Makefile To help you build and run your code. This section will go over some of the information in the Makefile relevant to building and running your unit tests.
In order to build and link our check_money.c
file we will need
-
to build
money.c
in order to getmoney.o
-
add all the necessary libraries needed by
check
during linking.
We already know how to build money.c
and money_check.c
. The libraries needed to link
our code with check
and all of check
's dependencies are
-
the
check
library -
the
m
library, math library -
the
pthread
library, POSIX threads library -
and the
rt
library, realtime extensions library
The command to link check_money
is
gcc money.o check_money.o -lcheck -lm -lpthread -lrt -o check_money_tests
The above line will create an executable with the name check_money_tests
linking all
the necessary dependent libraries to run the tests. To actually run
your tests execute check_money_tests
from a terminal. You should see the following output.
./check_money_tests Running suite(s): Money 100%: Checks: 1, Failures: 0, Errors: 0
How to generate a code coverage report from a run of your unit tests.
The source code for this lab includes a Makefile to help you build, run and profile your code. This section will go over some of the information in the Makefile relevant to profiling your code.
In order to profile your code and also generate code coverage reports for your tests
we are going to use gcovr
. For profiling and code coverage to work
all source files need to be build with profiling and coverage information enabled.
Thus, our build process will have to pass extra arguments
to the compiler. Here is an example for building money.c
that
enables both profiling and code coverage.
gcc -c -Wall -fprofile-arcs -ftest-coverage money.c
The above command will generate money.o
like before. However it
now also generates two extra files
-
money.gcda
-
money.gcno
These extra files contain extra information that profilers and
coverage tools (like gcovr
) can use to calculate coverage and
generate coverage reports.
The extra options -fprofile-arcs
and -ftest-coverage
must be used to build all
source files for which we are going to request code coverage reports.
So now that we have generated the extra information, let’s use gcovr
to generate our
code coverage report.
gcovr -r . --html --html-details -o coverage_report.html
The above command will generate code coverage reports in html and will store the generate
html in a file called coverage_report.html
. Open a browser and
navigate to coverage_report.html
to see the report.
To find out more information on gcovr
check your OS' manual page or see the online manual.