Imre Kis | a21712e | 2019-10-08 12:56:59 +0200 | [diff] [blame] | 1 | Implementing tests |
| 2 | ================== |
| 3 | |
| 4 | Concept of unit testing |
| 5 | ----------------------- |
| 6 | |
| 7 | First of all unit tests exercise the C code on a function level. The tests should call functions directly from the code under |
| 8 | tests and verify if their return values are matching the expected ones and the functions are behaving according to the |
| 9 | specification. |
| 10 | |
| 11 | Because of the function level testing the dependencies of the tested functions should be detached. This is done by mocking the |
| 12 | underlying layer. This provides an additional advantage of controlling and verifying all the call to the lower layer. |
| 13 | |
| 14 | |
| 15 | Adding new unit test suite |
| 16 | -------------------------- |
| 17 | |
| 18 | The first step is to define a new unit test suite. If a completely new module is being test the test suite definition should be |
| 19 | created in a separate ``.cmake`` file which is placed in the test files' directory. Otherwise the test definition can be added |
| 20 | to an existing ``.cmake`` file. These files should be included in the root ``CMakeLists.txt``. |
| 21 | |
| 22 | The ``UnitTest`` CMake module defines the ``unit_test_add_suite`` function so before using this function the module must be |
| 23 | included in the ``.cmake`` file. The function first requires a unique test name which will be test binary's name. The test |
| 24 | sources, include directories and macro definition are passed to the function in the matching arguments. CMake variables can be |
| 25 | used to reference files relative to common directories: |
| 26 | |
| 27 | - ``CMAKE_CURRENT_LIST_DIR`` - Relative to the ``.cmake`` file |
| 28 | - :cmake:variable:`UNIT_TEST_PROJECT_PATH` - Relative to the project's root directory |
| 29 | |
| 30 | .. code-block:: cmake |
| 31 | |
| 32 | # tests/new_module/new_test_suite.cmake |
| 33 | include(UnitTest) |
| 34 | |
| 35 | unit_test_add_suite( |
| 36 | NAME [unique test name] |
| 37 | SOURCES |
| 38 | [source files] |
| 39 | INCLUDE_DIRECTORIES |
| 40 | [include directories] |
| 41 | COMPILE_DEFINITIONS |
| 42 | [defines] |
| 43 | ) |
| 44 | |
| 45 | .. code-block:: cmake |
| 46 | |
| 47 | # Root CMakeLists.txt |
| 48 | include(tests/new_module/new_test_suite.cmake) |
| 49 | |
| 50 | Example test definition |
| 51 | ^^^^^^^^^^^^^^^^^^^^^^^ |
| 52 | |
| 53 | .. code-block:: cmake |
| 54 | |
| 55 | unit_test_add_suite( |
| 56 | NAME memcmp |
| 57 | SOURCES |
| 58 | ${CMAKE_CURRENT_LIST_DIR}/test_memcmp.cpp |
| 59 | ${CMAKE_CURRENT_LIST_DIR}/memcmp.yml |
| 60 | INCLUDE_DIRECTORIES |
| 61 | ${UNIT_TEST_PROJECT_PATH}/include |
| 62 | ${UNIT_TEST_PROJECT_PATH}/include/lib/libc/aarch64/ |
| 63 | ) |
| 64 | |
| 65 | |
| 66 | Using c-picker |
| 67 | -------------- |
| 68 | |
| 69 | c-picker is a simple tool used for detaching dependencies of the code under test. It can copy elements (i.e. functions, |
| 70 | variables, etc.) from the original source code into generated files. This way the developer can pick functions from compilation |
| 71 | units and surround them with a mocked environment. |
| 72 | |
| 73 | If a ``.yml`` file listed among source files the build system invokes c-picker and the generated ``.c`` file is implicitly added |
| 74 | to the source file list. |
| 75 | |
| 76 | Example .yml file |
| 77 | ^^^^^^^^^^^^^^^^^ |
| 78 | |
| 79 | In this simple example c-picker is instructed to copy the include directives and the ``memcmp`` function from the |
| 80 | ``lib/libc/memcmp.c`` file. The root directory of the source files referenced by c-picker is the project's root directory. |
| 81 | |
| 82 | .. code-block:: yaml |
| 83 | |
| 84 | elements: |
| 85 | - file: lib/libc/memcmp.c |
| 86 | type: include |
| 87 | - file: lib/libc/memcmp.c |
| 88 | type: function |
| 89 | name: memcmp |
| 90 | |
| 91 | |
| 92 | Writing unit tests |
| 93 | ------------------ |
| 94 | |
| 95 | Unit test code should be placed in ``.cpp`` files. |
| 96 | |
| 97 | Four-phase test pattern |
| 98 | ^^^^^^^^^^^^^^^^^^^^^^^ |
| 99 | |
| 100 | All tests cases should follow the four-phase test pattern. This consists of four simple steps that altogether ensure the |
| 101 | isolation between test cases. These steps follows below. |
| 102 | |
| 103 | - Setup |
| 104 | - Exercise |
| 105 | - Verify |
| 106 | - Teardown |
| 107 | |
| 108 | After the teardown step all global states should be the same as they were at the beginning of the setup step. |
| 109 | |
| 110 | CppUTest |
| 111 | ^^^^^^^^ |
| 112 | |
| 113 | CppUTest is an open source unit testing framework for C/C++. It is written in C++ so all the useful features of the language is |
| 114 | available while testing. It automatically collects and runs the defined ``TEST_GROUPS`` and provides an interface for |
| 115 | implementing the four-phase test pattern. Furthermore the framework has assertion macros for many variable types and test |
| 116 | scenarios. |
| 117 | |
| 118 | Include |
| 119 | ''''''' |
| 120 | |
| 121 | The unit test source files should include the CppUTest header after all other headers to avoid conflicts. |
| 122 | |
| 123 | .. code-block:: C++ |
| 124 | |
| 125 | // Other headers |
| 126 | // [...] |
| 127 | |
| 128 | #include "CppUTest/TestHarness.h" |
| 129 | |
| 130 | Test group |
| 131 | '''''''''' |
| 132 | |
| 133 | The next step is to define a test group. When multiple tests cases are written around testing the same function or couple |
| 134 | related functions these tests cases should be part of the same test group. Basically test cases in a test group share have same |
| 135 | setup/teardown sequence. In CppUTest the ``TEST_GROUP`` macro defines a new class which can contain member variables and |
| 136 | functions. Special setup/teardown function are defined using ``TEST_SETUP`` and ``TEST_TEARDOWN`` macros. These functions are |
| 137 | called before/after running each test case of the group so all the common initilization and cleanup code should go into these |
| 138 | functions. |
| 139 | |
| 140 | .. code-block:: C++ |
| 141 | |
| 142 | TEST_GROUP(List) { |
| 143 | TEST_SETUP() { |
| 144 | list = list_alloc(); |
| 145 | } |
| 146 | |
| 147 | TEST_TEARDOWN() { |
| 148 | list_cleanup(list); |
| 149 | } |
| 150 | |
| 151 | bool has_element(int value) { |
| 152 | for (int i = 0; i < list_count (list); i++) { |
| 153 | if (list_get(i) == value) { return true; } |
| 154 | } |
| 155 | return false; |
| 156 | } |
| 157 | |
| 158 | List* list; |
| 159 | }; |
| 160 | |
| 161 | |
| 162 | Test case |
| 163 | ''''''''' |
| 164 | |
| 165 | Test cases are defined by the ``TEST`` macro. This macro defines a child class of the test group's class so it can access the |
| 166 | member functions and variables of the test group. The test case's block itself is the body of the function of the child class. |
| 167 | |
| 168 | .. code-block:: C++ |
| 169 | |
| 170 | TEST(List, add_one) { |
| 171 | // Exercise |
| 172 | const int test_value = 5; |
| 173 | list_add(list, test_value); |
| 174 | |
| 175 | // Verify using CHECK_TRUE assertion and TEST_GROUP member function |
| 176 | CHECK_TRUE(has_element(test_value)); |
| 177 | } |
| 178 | |
| 179 | TEST(List, add_two) { |
| 180 | // Exercise |
| 181 | const int test_value1 = 5; |
| 182 | const int test_value2 = 6; |
| 183 | list_add(list, test_value1); |
| 184 | list_add(list, test_value2); |
| 185 | |
| 186 | // Verify |
| 187 | CHECK_TRUE(has_element(test_value1)); |
| 188 | CHECK_TRUE(has_element(test_value2)); |
| 189 | } |
| 190 | |
| 191 | CppUMock |
| 192 | ^^^^^^^^ |
| 193 | |
| 194 | During unit testing the dependencies of the tested functions should be replaced by stubs or mocks. When using mocks the |
| 195 | developer would usually like to check if the function was called with corrent parameters and would like to return controlled |
| 196 | values from the function. When a mocked function is called multiple times from the tested function maybe it should check or |
| 197 | return different values on each call. This is where CppUMock comes handy. |
| 198 | |
| 199 | All CppUMock interactions start with calling the ``mock()`` function. This returs a reference to the mocking system. At this |
| 200 | point the developer either wants to define expected or actual calls. This is achiveable by calling |
| 201 | ``expectOneCall(functionName)`` or ``expectNCalls(amount, functionName)`` or ``actualCall(functionName)`` member functions of |
| 202 | ``mock()`` call's return value. Registering expected calls are done in the test case before exercising the code and actual calls |
| 203 | happen from the mocked functions. |
| 204 | |
| 205 | After this point the following functions can be chained: |
| 206 | |
| 207 | - ``onObject(object)`` - In C++ it is usually the ``this`` pointer but it can be |
| 208 | useful in C too. |
| 209 | - ``with[type]Parameter(name, value)`` - Specifying and checking call parameters |
| 210 | - ``andReturnValue(result)`` - Specifying return value when defining expected |
| 211 | call |
| 212 | - ``return[type]Value()`` - Returning value from function |
| 213 | |
| 214 | The mocking system has two important functions. ``mock().checkExpectation()`` checks if all the expected calls have been |
| 215 | fulfilled and and the ``mock().clear()`` removes all the expected calls from CppUMock's registry. These two functions are |
| 216 | usually called from the ``TEST_TEARDOWN`` function because there should not be any crosstalk between test cases through the |
| 217 | mocking system. |
| 218 | |
| 219 | CppUMock's typical use-case is shown below by a simple example of the ``print_to_eeprom`` function. |
| 220 | |
| 221 | .. code-block:: C++ |
| 222 | |
| 223 | int eeprom_write(const char* str); /* From eeprom.h */ |
| 224 | |
| 225 | int printf_to_eeprom(const char* format, ...) { |
| 226 | char buffer[256]; |
| 227 | int length, written_bytes = 0, eeprom_write_result; |
| 228 | va_list args; |
| 229 | |
| 230 | va_start(args, format); |
| 231 | length = vsnprintf(buffer, sizeof(buffer), format, args); |
| 232 | va_end(args); |
| 233 | |
| 234 | if (length < 0) { |
| 235 | return length; |
| 236 | } |
| 237 | |
| 238 | while(written_bytes < length) { |
| 239 | eeprom_write_result = eeprom_write(&buffer[written_bytes]); |
| 240 | if (eeprom_write_result < 0) { |
| 241 | return eeprom_write_result; |
| 242 | } |
| 243 | written_bytes += eeprom_write_result; |
| 244 | } |
| 245 | |
| 246 | return written_bytes; |
| 247 | } |
| 248 | |
| 249 | Having the code snipped above a real life usage of the function would look like something shown in the following sequence |
| 250 | diagram. |
| 251 | |
| 252 | .. uml:: resources/sequence_print_to_eeprom.puml |
| 253 | |
| 254 | It would be really hard to test unit this whole system so all the lower layers should be separated and mock on the first |
| 255 | possible level. In the following example the ``print_to_eeprom`` function is being tested and the ``eeprom_write`` function is |
| 256 | mocked. In test cases where ``eeprom_write`` function is expected to be called the test case should first call the |
| 257 | ``expect_write`` function. This registers an expected call to CppUMocks internal database and when the call actually happens it |
| 258 | matches the call parameters with the entry in the database. It also returns the previously specified value. |
| 259 | |
| 260 | .. code-block:: C++ |
| 261 | |
| 262 | TEST_GROUP(printf_to_eeprom) { |
| 263 | TEST_TEARDOWN() { |
| 264 | mock().checkExpectations(); |
| 265 | mock().clear(); |
| 266 | } |
| 267 | |
| 268 | void expect_write(const char* str, int result) { |
| 269 | mock().expectOneCall("eeprom_write").withStringParameter("str", str). |
| 270 | andReturnValue(result); |
| 271 | } |
| 272 | }; |
| 273 | |
| 274 | /* Mocked function */ |
| 275 | int eeprom_write(const char* str) { |
| 276 | return mock().actualCall("eeprom_write").withStringParameter("str", str). |
| 277 | returnIntValue(); |
| 278 | } |
| 279 | |
| 280 | TEST(printf_to_eeprom, empty) { |
| 281 | LONGS_EQUAL(0, printf_to_eeprom("")) |
| 282 | } |
| 283 | |
| 284 | TEST(printf_to_eeprom, two_writes) { |
| 285 | expect_write("hello1hello2", 6); |
| 286 | expect_write("hello2", 6); |
| 287 | LONGS_EQUAL(12, printf_to_eeprom("hello%dhello%d", 1, 2)) |
| 288 | } |
| 289 | |
| 290 | TEST(printf_to_eeprom, error) { |
| 291 | expect_write("hello", -1); |
| 292 | LONGS_EQUAL(-1, printf_to_eeprom("hello")) |
| 293 | } |
| 294 | |
| 295 | This how the ``printf_to_eeprom/two_writes`` test case's sequence diagram looks like after mocking ``eeprom_write``. The test |
| 296 | case became able to check the parameters of multiple calls and it could return controlled values. |
| 297 | |
| 298 | .. uml:: resources/sequence_print_to_eeprom_mock.puml |
| 299 | |
| 300 | |
| 301 | Analyzing code coverage |
| 302 | ----------------------- |
| 303 | |
| 304 | The code coverage reports can be easily used for finding untested parts of the code. The two main parts of the coverage report |
| 305 | are the line coverage and the branch coverage. Line coverage shows that how many times the tests ran the given line of the |
| 306 | source code. It is beneficial to increase the line coverage however 100% line coverage is still not enough to consider the code |
| 307 | fully tested. |
| 308 | |
| 309 | Let's have a look on the following example. |
| 310 | |
| 311 | .. code-block:: C++ |
| 312 | |
| 313 | void set_pointer_value(unsigned int id, unsigned int value) { |
| 314 | unsigned int *pointer; |
| 315 | |
| 316 | if (id < MAX_ID) { |
| 317 | pointer = get_pointer(id); |
| 318 | } |
| 319 | |
| 320 | *pointer = value; |
| 321 | } |
| 322 | |
| 323 | The 100% line coverage is achievable by testing the function with an ``id`` value smaller than ``MAX_ID``. However if an ``id`` |
| 324 | larger than or equal to ``MAX_ID`` is used as a parameter of this function it will try to write to a memory address pointed by |
| 325 | an uninitialized variable. To catch untested conditions like this the branch coverage comes handy. It will show that only one |
| 326 | branch of the ``if`` statement has been tested as the condition was always true in the tests. |
| 327 | |
| 328 | |
| 329 | -------------- |
| 330 | |
| 331 | *Copyright (c) 2019-2021, Arm Limited. All rights reserved.* |