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