Summary

The lab assumes that you are familiar with compiling and linking C code and using make and Makefiles.

The lab covers

  1. How to write unit tests for C using check

  2. How to build and run your unit tests

  3. 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

money.h
#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 */
money.c
#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

Test Function Example
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.

Test Suite Creation Example
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.

check_money.c
#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

  1. to build money.c in order to get money.o

  2. 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

  1. the check library

  2. the m library, math library

  3. the pthread library, POSIX threads library

  4. 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

  1. money.gcda

  2. 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.