Testing

Table of contents
  1. Testing
    1. Creating useful tests
    2. A simple example
    3. Unit testing
    4. Testing functions directly
    5. Choosing test cases
    6. Testing and debugging
    7. A useful approach

Testing code is an extremely important part of the software development cycle. A modern car, jam-packed with electronics and sensors, may have upwards of 100 million lines of code. The fact that a bug in the code may cause a fatal accident means that every single block of code will be tested. Indeed, software engineers often write tests before they write the actual code. This is known as Test-Driven Development (TDD).

Creating useful tests

Testing is the process of checking that a program behaves as expected.

A useful test should:

  • provide a known input
  • check the output
  • clearly report whether the test passed or failed

Testing helps us catch bugs quickly and gives us confidence that future changes have not broken existing code.

A simple example

Suppose we have:

int add(int a, int b)
{
    return a + b;
}

A simple test might be:

if (add(2, 3) == 5) {
    printf("PASS\n");
} else {
    printf("FAIL\n");
}

The test uses a known input and checks that the result matches our expectation.

Unit testing

As programs become larger, it is useful to test functions directly rather than repeatedly entering input by hand.

For example, Activity 4 separates the reusable operations from both the application and the tests:

flowchart LR
    Ops[activity4_operations.c<br/>reusable operations]
    App[activity4_main.c<br/>interactive program]
    Tests[activity4_test_operations.c<br/>unit tests]

    App --> Ops
    Tests --> Ops

The interactive application is built from:

activity4_main.c + activity4_operations.c
    -> interactive application

The test program is built from:

activity4_test_operations.c + activity4_operations.c
    -> automated test executable

Both programs use exactly the same operations library.

This allows us to test the functions directly without typing input each time.

Testing functions directly

For example:

check_int("addition with positive values", 12, operation_add(7, 5));

The test supplies:

  • a known input
  • an expected result
  • the actual value returned by the function

The test program can then report:

PASS

or:

FAIL

automatically.

Choosing test cases

Good tests should include:

  • normal inputs
  • boundary values
  • unusual inputs
  • invalid inputs where appropriate

For example:

operation_add(7, 5)
operation_multiply(0, 100)
operation_divide(9, 2, &result)
operation_divide(10, 0, &result)

The division-by-zero case is particularly important because it checks behaviour that is easy to overlook.

Testing and debugging

Testing and debugging work together:

Testing finds a problem
Debugging finds the cause

A failed test tells us that something is wrong.

Tools such as printf() and breakpoints help us discover why it is wrong.

A useful approach

When developing a program:

  1. Write a small amount of code.
  2. Test it.
  3. Fix any bugs.
  4. Add the next feature.
  5. Test again.

Finding a bug in 20 lines of code is usually much easier than finding a bug in 2000 lines of code.

The goal of testing is not to prove that a program is perfect. The goal is to increase our confidence that it behaves correctly.


This site uses Just the Docs, a documentation theme for Jekyll.