aboutsummaryrefslogtreecommitdiff
path: root/docs/implementing_tests.rst
blob: fe3cb4a3ebf1cde365c1be8fde1a8b7151f6aba7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
Implementing tests
==================

Concept of unit testing
-----------------------

First of all unit tests exercise the C code on a function level. The tests should call functions directly from the code under
tests and verify if their return values are matching the expected ones and the functions are behaving according to the
specification.

Because of the function level testing the dependencies of the tested functions should be detached. This is done by mocking the
underlying layer. This provides an additional advantage of controlling and verifying all the call to the lower layer.


Adding new unit test suite
--------------------------

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
created in a separate ``.cmake`` file which is placed in the test files' directory. Otherwise the test definition can be added
to an existing ``.cmake`` file. These files should be included in the root ``CMakeLists.txt``.

The ``UnitTest`` CMake module defines the ``unit_test_add_suite`` function so before using this function the module must be
included in the ``.cmake`` file. The function first requires a unique test name which will be test binary's name. The test
sources, include directories and macro definition are passed to the function in the matching arguments. CMake variables can be
used to reference files relative to common directories:

- ``CMAKE_CURRENT_LIST_DIR`` - Relative to the ``.cmake`` file
- :cmake:variable:`UNIT_TEST_PROJECT_PATH` - Relative to the project's root directory

.. code-block:: cmake

  # tests/new_module/new_test_suite.cmake
  include(UnitTest)

  unit_test_add_suite(
  	NAME [unique test name]
  	SOURCES
  		[source files]
  	INCLUDE_DIRECTORIES
  		[include directories]
  	COMPILE_DEFINITIONS
  		[defines]
  )

.. code-block:: cmake

  # Root CMakeLists.txt
  include(tests/new_module/new_test_suite.cmake)

Example test definition
^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: cmake

  unit_test_add_suite(
  	NAME memcmp
  	SOURCES
  		${CMAKE_CURRENT_LIST_DIR}/test_memcmp.cpp
  		${CMAKE_CURRENT_LIST_DIR}/memcmp.yml
  	INCLUDE_DIRECTORIES
  		${UNIT_TEST_PROJECT_PATH}/include
  		${UNIT_TEST_PROJECT_PATH}/include/lib/libc/aarch64/
  )


Using c-picker
--------------

c-picker is a simple tool used for detaching dependencies of the code under test. It can copy elements (i.e. functions,
variables, etc.) from the original source code into generated files. This way the developer can pick functions from compilation
units and surround them with a mocked environment.

If a ``.yml`` file listed among source files the build system invokes c-picker and the generated ``.c`` file is implicitly added
to the source file list.

Example .yml file
^^^^^^^^^^^^^^^^^

In this simple example c-picker is instructed to copy the include directives and the ``memcmp`` function from the
``lib/libc/memcmp.c`` file. The root directory of the source files referenced by c-picker is the project's root directory.

.. code-block:: yaml

  elements:
  - file: lib/libc/memcmp.c
    type: include
  - file: lib/libc/memcmp.c
    type: function
    name: memcmp


Writing unit tests
------------------

Unit test code should be placed in ``.cpp`` files.

Four-phase test pattern
^^^^^^^^^^^^^^^^^^^^^^^

All tests cases should follow the four-phase test pattern. This consists of four simple steps that altogether ensure the
isolation between test cases. These steps follows below.

- Setup
- Exercise
- Verify
- Teardown

After the teardown step all global states should be the same as they were at the beginning of the setup step.

CppUTest
^^^^^^^^

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
available while testing. It automatically collects and runs the defined ``TEST_GROUPS`` and provides an interface for
implementing the four-phase test pattern. Furthermore the framework has assertion macros for many variable types and test
scenarios.

Include
'''''''

The unit test source files should include the CppUTest header after all other headers to avoid conflicts.

.. code-block:: C++

  // Other headers
  // [...]

  #include "CppUTest/TestHarness.h"

Test group
''''''''''

The next step is to define a test group. When multiple tests cases are written around testing the same function or couple
related functions these tests cases should be part of the same test group. Basically test cases in a test group share have same
setup/teardown sequence. In CppUTest the ``TEST_GROUP`` macro defines a new class which can contain member variables and
functions. Special setup/teardown function are defined using ``TEST_SETUP`` and ``TEST_TEARDOWN`` macros. These functions are
called before/after running each test case of the group so all the common initilization and cleanup code should go into these
functions.

.. code-block:: C++

  TEST_GROUP(List) {
  	TEST_SETUP() {
  		list = list_alloc();
  	}

  	TEST_TEARDOWN() {
  		list_cleanup(list);
  	}

  	bool has_element(int value) {
  		for (int i = 0; i < list_count (list); i++) {
  			if (list_get(i) == value) { return true; }
  		}
  		return false;
  	}

  	List* list;
  };


Test case
'''''''''

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
member functions and variables of the test group. The test case's block itself is the body of the function of the child class.

.. code-block:: C++

  TEST(List, add_one) {
  	// Exercise
  	const int test_value = 5;
  	list_add(list, test_value);

  	// Verify using CHECK_TRUE assertion and TEST_GROUP member function
  	CHECK_TRUE(has_element(test_value));
  }

  TEST(List, add_two) {
  	// Exercise
  	const int test_value1 = 5;
  	const int test_value2 = 6;
  	list_add(list, test_value1);
  	list_add(list, test_value2);

  	// Verify
  	CHECK_TRUE(has_element(test_value1));
  	CHECK_TRUE(has_element(test_value2));
  }

CppUMock
^^^^^^^^

During unit testing the dependencies of the tested functions should be replaced by stubs or mocks. When using mocks the
developer would usually like to check if the function was called with corrent parameters and would like to return controlled
values from the function. When a mocked function is called multiple times from the tested function maybe it should check or
return different values on each call. This is where CppUMock comes handy.

All CppUMock interactions start with calling the ``mock()`` function. This returs a reference to the mocking system. At this
point the developer either wants to define expected or actual calls. This is achiveable by calling
``expectOneCall(functionName)`` or ``expectNCalls(amount, functionName)`` or ``actualCall(functionName)`` member functions of
``mock()`` call's return value. Registering expected calls are done in the test case before exercising the code and actual calls
happen from the mocked functions.

After this point the following functions can be chained:

- ``onObject(object)`` - In C++ it is usually the ``this`` pointer but it can be
  useful in C too.
- ``with[type]Parameter(name, value)`` - Specifying and checking call parameters
- ``andReturnValue(result)`` - Specifying return value when defining expected
  call
- ``return[type]Value()`` - Returning value from function

The mocking system has two important functions. ``mock().checkExpectation()`` checks if all the expected calls have been
fulfilled and and the ``mock().clear()`` removes all the expected calls from CppUMock's registry. These two functions are
usually called from the ``TEST_TEARDOWN`` function because there should not be any crosstalk between test cases through the
mocking system.

CppUMock's typical use-case is shown below by a simple example of the ``print_to_eeprom`` function.

.. code-block:: C++

  int eeprom_write(const char* str); /* From eeprom.h */

  int printf_to_eeprom(const char* format, ...) {
  	char buffer[256];
  	int length, written_bytes = 0, eeprom_write_result;
  	va_list args;

  	va_start(args, format);
  	length = vsnprintf(buffer, sizeof(buffer), format, args);
  	va_end(args);

  	if (length < 0) {
  		return length;
  	}

  	while(written_bytes < length) {
  		eeprom_write_result = eeprom_write(&buffer[written_bytes]);
  		if (eeprom_write_result < 0) {
  			return eeprom_write_result;
  		}
  		written_bytes += eeprom_write_result;
  	}

  	return written_bytes;
  }

Having the code snipped above a real life usage of the function would look like something shown in the following sequence
diagram.

.. uml:: resources/sequence_print_to_eeprom.puml

It would be really hard to test unit this whole system so all the lower layers should be separated and mock on the first
possible level. In the following example the ``print_to_eeprom`` function is being tested and the ``eeprom_write`` function is
mocked. In test cases where ``eeprom_write`` function is expected to be called the test case should first call the
``expect_write`` function. This registers an expected call to CppUMocks internal database and when the call actually happens it
matches the call parameters with the entry in the database. It also returns the previously specified value.

.. code-block:: C++

  TEST_GROUP(printf_to_eeprom) {
  	TEST_TEARDOWN() {
  		mock().checkExpectations();
  		mock().clear();
  	}

  	void expect_write(const char* str, int result) {
  		mock().expectOneCall("eeprom_write").withStringParameter("str", str).
  			andReturnValue(result);
  	}
  };

  /* Mocked function */
  int eeprom_write(const char* str) {
  	return mock().actualCall("eeprom_write").withStringParameter("str", str).
  		returnIntValue();
  }

  TEST(printf_to_eeprom, empty) {
  	LONGS_EQUAL(0, printf_to_eeprom(""))
  }

  TEST(printf_to_eeprom, two_writes) {
  	expect_write("hello1hello2", 6);
  	expect_write("hello2", 6);
  	LONGS_EQUAL(12, printf_to_eeprom("hello%dhello%d", 1, 2))
  }

  TEST(printf_to_eeprom, error) {
  	expect_write("hello", -1);
  	LONGS_EQUAL(-1, printf_to_eeprom("hello"))
  }

This how the ``printf_to_eeprom/two_writes`` test case's sequence diagram looks like after mocking ``eeprom_write``. The test
case became able to check the parameters of multiple calls and it could return controlled values.

.. uml:: resources/sequence_print_to_eeprom_mock.puml


Analyzing code coverage
-----------------------

The code coverage reports can be easily used for finding untested parts of the code. The two main parts of the coverage report
are the line coverage and the branch coverage. Line coverage shows that how many times the tests ran the given line of the
source code. It is beneficial to increase the line coverage however 100% line coverage is still not enough to consider the code
fully tested.

Let's have a look on the following example.

.. code-block:: C++

  void set_pointer_value(unsigned int id, unsigned int value) {
  	unsigned int *pointer;

  	if (id < MAX_ID) {
  		pointer = get_pointer(id);
  	}

  	*pointer = value;
  }

The 100% line coverage is achievable by testing the function with an ``id`` value smaller than ``MAX_ID``. However if an ``id``
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
an uninitialized variable. To catch untested conditions like this the branch coverage comes handy. It will show that only one
branch of the  ``if`` statement has been tested as the condition was always true in the tests.


--------------

*Copyright (c) 2019-2021, Arm Limited. All rights reserved.*