Now that we ve identified the scope of the unit test, let s look at some unit testing frameworks to explore how they can be used to increase the quality of our software. The following are the frameworks we ll review.
Brew your own
C unit test (cut) system
Embedded unit test
expect
Building your own simple unit test framework is not difficult to do. Even the simplest architecture can yield great benefits. Let s look at a simple architecture for testing a software unit.
Consider that we have a simple stack module with an API consisting of the following:
typedef struct { ... } stack_t; int stackCreate(stack_t *stack, int stackSize); int stackPush(stack_t *stack, int element); int stackPop(stack_t *stack, int *element); int stackDestroy(stack_t *stack);
This very simple LIFO stack API permits us to create a new stack, push an element on the stack, pop an element from the stack, and finally destroy the stack. Listing 24.1 shows the code ( stack.c ) for this simple module, and Listing 24.2 shows the header file ( stack.h ).
1: #include <stdlib.h> 2: #include "stack.h" 3: 4: 5: int stackCreate(stack_t *stack, int stackSize) 6: { 7: if ((stackSize == 0) (stackSize > 1024)) return -1; 8: 9: stack->storage = (int *)malloc(sizeof(int) * stackSize); 10: 11: if (stack->storage == (void *)0) return -1; 12: 13: stack->state = STACK_CREATED; 14: stack->max = stackSize; 15: stack->index = 0; 16: 17: return 0; 18: } 19: 20: 21: int stackPush(stack_t *stack, int element) 22: { 23: if (stack == (stack_t *)NULL) return -1; 24: if (stack->state != STACK_CREATED) return -1; 25: if (stack->index >= stack->max) return -1; 26: 27: stack->storage[stack->index++] = element; 28: 29: return 0; 30: } 31: 32: 33: int stackPop(stack_t *stack, int *element) 34: { 35: if (stack == (stack_t *)NULL) return -1; 36: if (stack->state != STACK_CREATED) return -1; 37: if (stack->index == 0) return -1; 38: 39: *element = stack->storage[stack->index]; 40: 41: return 0; 42: } 43: 44: 45: int stackDestroy(stack_t *stack) 46: { 47: if (stack == (stack_t *)NULL) return -1; 48: if (stack->state != STACK_CREATED) return -1; 49: 50: stack->state = 0; 51: free((void *)stack->storage); 52: 53: return 0; 54: }
1: #define STACK_CREATED 0xFAF32000 2: 3: typedef struct { 4: 5: int state; 6: int index; 7: int max; 8: int *storage; 9: 10: } stack_t; 11: 12: 13: int stackCreate(stack_t *stack, int stackSize); 14: 15: int stackCreate(stack_t *stack, int element); 16: 17: int stackPop(stack_t *stack, int *element); 18: 19: int stackDestroy(stack_t *stack);
Let s look at a simple regression that when built with this stack module can be used to verify that it works as expected. Since there are many individual tests that can be used to validate this module, we ll concentrate on just a few to illustrate the approach.
First, in this regression, we provide two infrastructure functions as wrappers for the regression. The first is a simple main function that invokes each of the tests, and the second is a result checking function. The result checking function simply tests the result input, and if zero, the test failed, otherwise the test passed. This function is shown in Listing 24.4 (we ll look at its use shortly).
We declare a failed integer, which will be used to keep track of the number of actual failures. This is used by our main , which allows it to determine if the regression passed or failed. The checkResult function ( Listing 24.3) takes two inputs; a test number and the individual test result. If the test result is zero, then we mark the test as failed (and increment our failed count); otherwise the test passed (result is nonzero), and we indicate this.
1: int failed; 2: 3: void checkResult(int testnum, int result) 4: { 5: if (result == 0) { 6: printf("*** Failed test number %d\n", testnum); 7: failed++; 8: } else { 9: printf("Test number %2d passed.\n", testnum); 10: } 11: }
Our main program simply calls our regression tests in order, clearing the failed count (as shown in Listing 24.4).
1: int main() 2: { 3: 4: failed = 0; 5: test1(); 6: 7: return 0; 8: }
Now let s look at a regression that focuses on creation and destruction of stacks. As we saw in the stack module source, there are numerous ways that a stack creation and destruction can fail. This test tries to address each of them so that we can convince ourselves that it s coded properly (see Listing 24.5).
In this regression, we call an API function with a set of input data and then check the result. In some cases, we pass good data, and in others we pass data that will cause the function to exit with a failure status. Consider lines 8 “9, which test stack creation with a null stack element. This creation should fail and return -1. At line 9, we call checkResult with our test number (first argument) and then the test result as argument two. Note here that we test the ret variable with -1, since that s what we expect for this failure case. If ret wasn t -1, then the expression results in 0, indicating that the test failed. Otherwise, if ret is -1, the expression reduces to 1, and the result is a pass.
Our regression also explores the stack structure in order to ensure that the creation function has properly initialized the internal elements. At line 20, we check the internal state variable to ensure that it has been properly initialized with STACK_CREATED .
1: void test1(void) 2: { 3: stack_t myStack; 4: int ret; 5: 6: failed = 0; 7: 8: ret = stackCreate(0, 0); 9: checkResult(0, (ret == -1)); 10: 11: ret = stackCreate(&myStack, 0); 12: checkResult(1, (ret == -1)); 13: 14: ret = stackCreate(&myStack, 65536); 15: checkResult(2, (ret == -1)); 16: 17: ret = stackCreate(&myStack, 1024); 18: checkResult(3, (ret == 0)); 19: 20: checkResult(4, (myStack.state == STACK_CREATED)); 21: 22: checkResult(5, (myStack.index == 0)); 23: 24: checkResult(6, (myStack.max == 1024)); 25: 26: checkResult(7, (myStack.storage != (int *)0)); 27: 28: ret = stackDestroy(0); 29: checkResult(8, (ret == -1)); 30: 31: ret = stackDestroy(&myStack); 32: checkResult(9, (ret == 0)); 33: 34: checkResult(10, (myStack.state != STACK_CREATED)); 35: 36: if (failed == 0) printf("test1 passed.\n"); 37: else printf("test1 failed\n"); 38: }
At the end of this simple regression, we indicate whether the entire test passed (all individual tests passed) or failed. A sample run of the regression is illustrated as:
# gcc -Wall -o test regress.c stack.c # ./test Test number 0 passed. Test number 1 passed. Test number 2 passed. Test number 3 passed. Test number 4 passed. Test number 5 passed. Test number 6 passed. Test number 7 passed. Test number 8 passed. Test number 9 passed. Test number 10 passed. test1 passed. #
While this method is quite simple, it s also very effective and permits the quick revalidation of a unit after changes are made. Once we ve run our regression, we can deliver it safely with the understanding that we re less likely to break the software that uses it.