Lecture 5: Testing
\( \newcommand\bigO{\mathrm{O}} \)
1 Complete Test
Testing every possible path in every possible situation. For example, we can test the following function on all
char
inputs, there are only 256 characters after all:// If `c` is a letter, returns the corresponding upper-case letter. Otherwise, // returns `c`. char ToUpper(char c) { if ('a' <= c && c <= 'z') { return c - 'a' + 'A'; } return c; } // Returns if the input is an alphabetical symbol bool IsAlpha(char c) { return 'A' <= ToUpper(c) && ToUpper(c) <= 'Z'; }
- Complete tests are infeasible in general! For example, testing a distance function computing \( (x_1 - x_2)^2 + (y_1 - y_2)^2 \) would require \( 2^{32 \times 4} = 2^{128} \approx 10^{38} \) cases. That is a hundred trillion trillion trillion cases.
- The best we can hope for is trying to approximate a complete test by testing various types of cases.
- Another alternative is to "simulate" running all cases. This is in the domain of program analysis. You may learn about this in the future when you take Compilers or Programming Languages.
- The more rigorous testing a program can "pass", the more confidence is gained that the program is "bulletproof" (handles every error as expected).
2 Unit Testing
- Testing individual pieces (units) of a program (small and large) to ensure correct behavior.
- Good software practice is structuring your program into modular units.
- Good tests generally cover interesting cases including:
- Normal Cases: Cases where the input is generally what we expect.
- Boundary Cases: Cases that test the edge values of possible inputs.
- Error Cases: Invalid cases (like passing a string instead of an
int).
- C++ catches most of these types of errors during the compilation process.
- Other languages, such as Python, may need to rigorously check invalid input cases.
2.1 Property Testing
Sometimes a function has some properties. For example, if we have a maximum
function max
, then max(l) >= l[i]
holds for all 0 <= i < l.size()
. We
can write tests that generate random inputs and test these properties if we
want.
3 Test Suite
- A program containing various tests confirming certain behavior.
- A good way to automate tests.
- Very important for large-scale projects where other engineers may make a change that affects functionality of other existing (or new) functionality.
- We've seen testing programs in several of our lab assignments testing students' submissions throughout the quarter.
3.1 Metrics for Testing
Coverage is a usually common metric to see how well a test suite functions. The most common versions of it are:
- Line coverage
- How many lines of the code are run in tests
- Branch coverage
- How many different branches are taken
4 Example
Writing our own simple program using tddFuncs, which tests a function that takes four integers and returns the largest.
#include <iostream> #include <string> #include "tddFuncs.h" using namespace std; int biggest (int a, int b, int c, int d) { int biggest = 0; if (a >= b && a >= c && a >= d) return a; if (b >= a && b >= c && b >= d) return b; if (c >= a && c >= b && c >= d) return c; return d; } int isPositive(int a) { return a >= 0; } int main() { ASSERT_EQUALS(4, biggest(1,2,3,4)); ASSERT_EQUALS(4, biggest(1,2,4,3)); ASSERT_EQUALS(4, biggest(1,4,2,3)); ASSERT_EQUALS(4, biggest(4,1,2,3)); ASSERT_EQUALS(4, biggest(4,4,4,4)); ASSERT_EQUALS(-1, biggest(-1,-2,-3,-4)); ASSERT_EQUALS(0, biggest(-1,0,-3,-4)); ASSERT_TRUE(isPositive(1)); ASSERT_TRUE(isPositive(2)); ASSERT_TRUE(isPositive(0)); ASSERT_TRUE(!isPositive(-1)); ASSERT_TRUE(!isPositive(-20)); return 0; }
5 Test-Driven Development
- Write test cases that describe what the intended behavior of a unit of
software should BEFORE implementing the functionality.
- Defines the requirements of your piece of software.
- Implement the details of the functionality with the intention of passing the tests.
- Repeat until the tests pass.
- Imagine large software products where dozens of engineers are trying
to add new features / implement optimizations all at the same time.
- Having a "suite" of tests before deploying software to the public is essential.
- Someone may modify changes that work for a current version, but breaks functionality in another version
- Rigorous tests enable confidence in the stability in software.
5.1 Mocking
Sometimes our code depends on some external classes or functions (such as file I/O, databases, connecting to network). To test a piece of code in isolation, we create mock versions of those classes and functions that work for the test inputs. The mock classes allow us to reduce the parts of code we need to look at when a test fails.