Testing
Table of contents
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:
- Write a small amount of code.
- Test it.
- Fix any bugs.
- Add the next feature.
- 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.