blob: fe3cb4a3ebf1cde365c1be8fde1a8b7151f6aba7 [file] [log] [blame]
Imre Kisa21712e2019-10-08 12:56:59 +02001Implementing tests
2==================
3
4Concept of unit testing
5-----------------------
6
7First of all unit tests exercise the C code on a function level. The tests should call functions directly from the code under
8tests and verify if their return values are matching the expected ones and the functions are behaving according to the
9specification.
10
11Because of the function level testing the dependencies of the tested functions should be detached. This is done by mocking the
12underlying layer. This provides an additional advantage of controlling and verifying all the call to the lower layer.
13
14
15Adding new unit test suite
16--------------------------
17
18The first step is to define a new unit test suite. If a completely new module is being test the test suite definition should be
19created in a separate ``.cmake`` file which is placed in the test files' directory. Otherwise the test definition can be added
20to an existing ``.cmake`` file. These files should be included in the root ``CMakeLists.txt``.
21
22The ``UnitTest`` CMake module defines the ``unit_test_add_suite`` function so before using this function the module must be
23included in the ``.cmake`` file. The function first requires a unique test name which will be test binary's name. The test
24sources, include directories and macro definition are passed to the function in the matching arguments. CMake variables can be
25used 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
50Example 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
66Using c-picker
67--------------
68
69c-picker is a simple tool used for detaching dependencies of the code under test. It can copy elements (i.e. functions,
70variables, etc.) from the original source code into generated files. This way the developer can pick functions from compilation
71units and surround them with a mocked environment.
72
73If a ``.yml`` file listed among source files the build system invokes c-picker and the generated ``.c`` file is implicitly added
74to the source file list.
75
76Example .yml file
77^^^^^^^^^^^^^^^^^
78
79In 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
92Writing unit tests
93------------------
94
95Unit test code should be placed in ``.cpp`` files.
96
97Four-phase test pattern
98^^^^^^^^^^^^^^^^^^^^^^^
99
100All tests cases should follow the four-phase test pattern. This consists of four simple steps that altogether ensure the
101isolation between test cases. These steps follows below.
102
103- Setup
104- Exercise
105- Verify
106- Teardown
107
108After the teardown step all global states should be the same as they were at the beginning of the setup step.
109
110CppUTest
111^^^^^^^^
112
113CppUTest is an open source unit testing framework for C/C++. It is written in C++ so all the useful features of the language is
114available while testing. It automatically collects and runs the defined ``TEST_GROUPS`` and provides an interface for
115implementing the four-phase test pattern. Furthermore the framework has assertion macros for many variable types and test
116scenarios.
117
118Include
119'''''''
120
121The 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
130Test group
131''''''''''
132
133The next step is to define a test group. When multiple tests cases are written around testing the same function or couple
134related functions these tests cases should be part of the same test group. Basically test cases in a test group share have same
135setup/teardown sequence. In CppUTest the ``TEST_GROUP`` macro defines a new class which can contain member variables and
136functions. Special setup/teardown function are defined using ``TEST_SETUP`` and ``TEST_TEARDOWN`` macros. These functions are
137called before/after running each test case of the group so all the common initilization and cleanup code should go into these
138functions.
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
162Test case
163'''''''''
164
165Test cases are defined by the ``TEST`` macro. This macro defines a child class of the test group's class so it can access the
166member 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
191CppUMock
192^^^^^^^^
193
194During unit testing the dependencies of the tested functions should be replaced by stubs or mocks. When using mocks the
195developer would usually like to check if the function was called with corrent parameters and would like to return controlled
196values from the function. When a mocked function is called multiple times from the tested function maybe it should check or
197return different values on each call. This is where CppUMock comes handy.
198
199All CppUMock interactions start with calling the ``mock()`` function. This returs a reference to the mocking system. At this
200point 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
203happen from the mocked functions.
204
205After 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
214The mocking system has two important functions. ``mock().checkExpectation()`` checks if all the expected calls have been
215fulfilled and and the ``mock().clear()`` removes all the expected calls from CppUMock's registry. These two functions are
216usually called from the ``TEST_TEARDOWN`` function because there should not be any crosstalk between test cases through the
217mocking system.
218
219CppUMock'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
249Having the code snipped above a real life usage of the function would look like something shown in the following sequence
250diagram.
251
252.. uml:: resources/sequence_print_to_eeprom.puml
253
254It would be really hard to test unit this whole system so all the lower layers should be separated and mock on the first
255possible level. In the following example the ``print_to_eeprom`` function is being tested and the ``eeprom_write`` function is
256mocked. 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
258matches 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
295This how the ``printf_to_eeprom/two_writes`` test case's sequence diagram looks like after mocking ``eeprom_write``. The test
296case 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
301Analyzing code coverage
302-----------------------
303
304The code coverage reports can be easily used for finding untested parts of the code. The two main parts of the coverage report
305are the line coverage and the branch coverage. Line coverage shows that how many times the tests ran the given line of the
306source code. It is beneficial to increase the line coverage however 100% line coverage is still not enough to consider the code
307fully tested.
308
309Let'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
323The 100% line coverage is achievable by testing the function with an ``id`` value smaller than ``MAX_ID``. However if an ``id``
324larger 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
325an uninitialized variable. To catch untested conditions like this the branch coverage comes handy. It will show that only one
326branch 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.*