Unit test example
Learning outcomes
- Witness a practical example of using unit testing
- Understand how to apply unit testing to reduce coding time
Context
- We assume that you are starting from the temperature sensing example (see 11-lab1)
- This shows the raw temperature but not the converted value
- We want to convert the raw value using \[ x / 100 - 39.6 \]
- Note that the raw value
$x$ is an integer but when we divide by 100 we expect to be dealing with a decimal number - For efficiency on embedded devices, we often do all the computation in integer numbers rather than floating point as there is no floating processor
Creating a unit test environment
- Test code needs to be separate from other code
- If you want to be able to test your function natively, it’s a good idea to separate it from your main Contiki project code
- There are many possible ways to set this up, we show one
examples | \- showtemp | \- test
Here, showtemp
is a sub directory of examples
and test
is a subdirectory of showtemp
.
- To set-up the directory structure, use something like:
cd ~/contiki-ng/examples
mkdir showtemp
cd showtemp
mkdir test
Separating out the function in question
- Rather than make your main source file longer and longer (and thus harder and harder to understand), we want to separate out functions into separate files
- There are several steps to this process:
- cut and paste the function into a separate
.c
file - create a similarly named
.h
file that includes any type definitions that are going to be common to the main program and the function and also any prototypes.
- cut and paste the function into a separate
How does this look?
- Let’s say we call the function
convert_temperature()
- We can name the source file
convtemp.c
(you can also have longer names if you like) - The associated header will then be called
convtemp.h
Start with some test code
A key idea of unit testing is to create the test code first.
In the test
directory, create a test file test_temp.c
with the following content:
#include <stdio.h>
#include <assert.h>
#include "types.h" // Note!
#include "convtemp.h"
void test_temp_calc(void) {
}
int main()
{
test_temp_calc();
return 0;
}
Note that we need types.h
so that we can define the types that ordinarily are defined by Contiki:
#ifndef CONTIKI
typedef unsigned short int uint16_t;
typedef short int int16_t;
#endif
We don’t want to accidentally include these type definitions when Contiki is being used, so #ifndef
says “only include this bit when CONTIKI is not defined”.
At this stage, we haven’t defined a test.
So we now need to think how we would like to call the function that converts the temperature.
If we want to avoid floating point numbers, we need to split the result into two parts: an integer part and a fractional part.
We can do this in C by returning a data structure but it reduces the amount of copying if we pass this data-structure as a parameter as well.
The prototype for the function is included in the header convtemp.h
struct decimal {
int16_t integer_part;
uint16_t fractional_part;
};
struct decimal* convert_temperature(struct decimal* dp, uint16_t v);
and our tests can then be:
void test_temp_calc(void) {
struct decimal result;
convert_temperature(&result, 6666);
assert(result.integer_part == 66 - 39);
assert(result.fractional_part == 66 - 60);
convert_temperature(&result, 0);
assert(result.integer_part == -39);
assert(result.fractional_part == 60);
}
Finally we need a Makefile in the test
directory:
CFLAGS += -I ..
test: test_temp
./test_temp
test_temp: test_temp.c ../convtemp.o ../convtemp.h
$(CC) $(CFLAGS) -o test_temp test_temp.c ../convtemp.o
Check that the test fails when it should
We begin by defining an empty function that just returns zero always. The following code should go into convtemp.c
#include "test/types.h"
#include "convtemp.h"
#include "stdlib.h"
struct decimal *convert_temperature(struct decimal *dp, uint16_t v)
{
dp->integer_part = 0;
dp->fractional_part = 0;
return dp;
}
The above code should fail and when we run make
from the test
directory. We get:
test_temp: test_temp.c:10: test_temp_calc: Assertion `result.integer_part == 66 - 39' failed.
The above says that the test failed at line 10.
First go at conversion
The following is a (wrong!) attempt at converting the temperature:
struct decimal *convert_temperature(struct decimal *dp, uint16_t v)
{
v = v - 3960;
dp->integer_part = v / 100;
dp->fractional_part = v % 100;
return dp;
}
which produces:
./test_temp test_temp: test_temp.c:16: test_temp_calc: Assertion `result.integer_part == -39' failed.
We are getting somewhere because some of the tests passed. However the integer part for the second test is clearly wrong. Pause for a second here and see if you can think of why this problem is occurring.
Second go
Here’s another (still wrong!) attempt:
struct decimal *convert_temperature(struct decimal *dp, uint16_t v)
{
int v1;
v1 = v - 3960;
dp->integer_part = v1 / 100;
dp->fractional_part = v1 % 100;
return dp;
}
By the way, if you haven’t worked it out, the reason why the previous test failed was because we were trying to store a negative number in an unsigned integer.
When we try this out, we get:
test_temp: test_temp.c:17: test_temp_calc: Assertion `result.fractional_part == 60' failed.
The problem here is quite subtle.
It turns out that we need to take the absolute value of v1
.
Final version
struct decimal *convert_temperature(struct decimal *dp, uint16_t v)
{
int v1;
v1 = v - 3960;
dp->integer_part = v1 / 100;
dp->fractional_part = abs(v1) % 100;
return dp;
}