mariadb  10.6.1
About: MariaDB is a relational database management system (a drop-in replacement for MySQL). Beta release (10.6 series).
  Fossies Dox: mariadb-10.6.1.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

Testing C and C++ using MyTAP

Introduction

Unit tests are used to test individual components of a system. In contrast, functional tests usually test the entire system. The rationale is that each component should be correct if the system is to be correct. Unit tests are usually small pieces of code that tests an individual function, class, a module, or other unit of the code.

Observe that a correctly functioning system can be built from "faulty" components. The problem with this approach is that as the system evolves, the bugs surface in unexpected ways, making maintenance harder.

The advantages of using unit tests to test components of the system are several:

  • The unit tests can make a more thorough testing than the functional tests by testing correctness even for pathological use (which shouldn't be present in the system). This increases the overall robustness of the system and makes maintenance easier.
  • It is easier and faster to find problems with a malfunctioning component than to find problems in a malfunctioning system. This shortens the compile-run-edit cycle and therefore improves the overall performance of development.
  • The component has to support at least two uses: in the system and in a unit test. This leads to more generic and stable interfaces and in addition promotes the development of reusable components.

For example, the following are typical functional tests:

  • Does transactions work according to specifications?
  • Can we connect a client to the server and execute statements?

In contrast, the following are typical unit tests:

  • Can the 'String' class handle a specified list of character sets?
  • Does all operations for 'my_bitmap' produce the correct result?
  • Does all the NIST test vectors for the AES implementation encrypt correctly?

Writing unit tests

The purpose of writing unit tests is to use them to drive component development towards a solution that passes the tests. This means that the unit tests has to be as complete as possible, testing at least:

  • Normal input
  • Borderline cases
  • Faulty input
  • Error handling
  • Bad environment

Normal input

This is to test that the component have the expected behaviour. This is just plain simple: test that it works. For example, test that you can unpack what you packed, adding gives the sum, pincing the duck makes it quack.

This is what everybody does when they write tests.

Borderline cases

If you have a size anywhere for your component, does it work for size 1? Size 0? Sizes close to UINT_MAX?

It might not be sensible to have a size 0, so in this case it is not a borderline case, but rather a faulty input (see Faulty input).

Faulty input

Does your bitmap handle 0 bits size? Well, it might not be designed for it, but is should not crash the application, but rather produce an error. This is called defensive programming.

Unfortunately, adding checks for values that should just not be entered at all is not always practical: the checks cost cycles and might cost more than it's worth. For example, some functions are designed so that you may not give it a null pointer. In those cases it's not sensible to pass it NULL just to see it crash.

Since every experienced programmer add an assert() to ensure that you get a proper failure for the debug builds when a null pointer passed (you add asserts too, right?), you will in this case instead have a controlled (early) crash in the debug build.

Error handling

This is testing that the errors your component is designed to give actually are produced. For example, testing that trying to open a non-existing file produces a sensible error code.

Environment

Sometimes, modules has to behave well even when the environment fails to work correctly. Typical examples are when the computer is out of dynamic memory or when the disk is full. You can emulate this by replacing, e.g., malloc() with your own version that will work for a while, but then fail. Some things are worth to keep in mind here:

  • Make sure to make the function fail deterministically, so that you really can repeat the test.
  • Make sure that it doesn't just fail immediately. The unit might have checks for the first case, but might actually fail some time in the near future.

Writing unit tests

In this section we will give some advice on how to structure the unit tests to make the development run smoothly. The basic structure of a test is:

  • Plan
  • Test
  • Report

Plan the test

Planning the test means telling how many tests there are. In the event that one of the tests causes a crash, it is then possible to see that there are fewer tests than expected, and print a proper error message.

To plan a test, use the plan() function in the following manner:

int main(int argc, char *argv[])
{
plan(5);
.
.
.
}
void plan(int const count)
Set number of tests that is planned to execute.
Definition: tap.c:187
* argc
static char *** argv
#define main
Definition: lib_sql.cc:25

If you don't call the plan() function, the number of tests executed will be printed at the end. This is intended to be used while developing the unit and you are constantly adding tests. It is not indented to be used after the unit has been released.

Execute the test

To report the status of a test, the ok() function is used in the following manner:

int main(int argc, char *argv[])
{
plan(5);
ok(ducks == paddling_ducks,
"%d ducks did not paddle", ducks - paddling_ducks);
.
.
.
}
struct ok ok
Report test result as a TAP line.

This will print a test result line on the standard output in TAP format, which allows TAP handling frameworks (like Test::Harness) to parse the status of the test.

Report the result of the test

At the end, a complete test report should be written, with some statistics. If the test returns EXIT_SUCCESS, all tests were successful, otherwise at least one test failed.

To get a TAP compliant output and exit status, report the exit status in the following manner:

int main(int argc, char *argv[])
{
plan(5);
ok(ducks == paddling_ducks,
"%d ducks did not paddle", ducks - paddling_ducks);
.
.
.
return exit_status();
}
int exit_status()
Print summary report and return exit status.
Definition: tap.c:283

Ways to not do unit testing

In this section, we'll go through some quite common ways to write tests that are not a good idea.

Doing breadth-first testing

If you're writing a library with several functions, don't test all functions using size 1, then all functions using size 2, etc. If a test for size 42 fails, you have no easy way of tracking down why it failed.

It is better to concentrate on getting one function to work at a time, which means that you test each function for all sizes that you think is reasonable. Then you continue with the next function, doing the same. This is usually also the way that a library is developed (one function at a time) so stick to testing that is appropriate for now the unit is developed.

Writing unnecessarily large tests

Don't write tests that use parameters in the range 1-1024 unless you have a very good reason to believe that the component will succeed for 562 but fail for 564 (the numbers picked are just examples).

It is very common to write extensive tests "just to be safe." Having a test suite with a lot of values might give you a warm fuzzy feeling, but it doesn't really help you find the bugs. Good tests fail; seriously, if you write a test that you expect to succeed, you don't need to write it. If you think that it might fail, then you should write it.

Don't take this as an excuse to avoid writing any tests at all "since I make no mistakes" (when it comes to this, there are two kinds of people: those who admit they make mistakes, and those who don't); rather, this means that there is no reason to test that using a buffer with size 100 works when you have a test for buffer size 96.

The drawback is that the test suite takes longer to run, for little or no benefit. It is acceptable to do a exhaustive test if it doesn't take too long to run and it is quite common to do an exhaustive test of a function for a small set of values. Use your judgment to decide what is excessive: your milage may vary.