Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

Unit test example

Learning outcomes

  1. Witness a practical example of using unit testing
  2. 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:
    1. cut and paste the function into a separate .c file
    2. 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.

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;
}

Think about the maths

Perhaps it would have been better to understand the maths involved before trying to write the code.

That is, we wish to find the integer solutions $a$, $b$ to: \[ a + b/100 = x / 100 - 39.6 \] for integer values of $x$ in the range $[0, 65536)$.

I leave it to the reader to try and find the full solution. Possibly this solution will be neater than our final version above!

Summary

Although it can be daunting to set up your first test case, once you have done it, additional test cases are much easier.

The advantage of working this way is that it becomes much faster to develop correct code even though it may be slightly slower to begin with. I encourage you to stick with it!

Further exercises

As a further exercise, you might like to test out the next function that we need to write, which is to print out the integer and fractional part on the screen using printf. As a first go, you may find it easier just to manually check that it prints the right output. It is possible to automatically check the output but this requires you to “mock” printf.