C Unit Test System


The C Unit Test system, or cut for short, is a simple architecture for building unit test applications. Cut provides a simple environment for the integration of unit tests, with a set of tools that are used to pull together tests and then build a main framework to invoke and report them. Cut parses the unit test files and then builds them together into a single image, performing each of the provided unit tests in order.

Cut provides a utility, written in Python, called cutgen.py . This utility parses the unit test source files and then builds a main function around the tests that are to be run. The unit test files must be written in a certain way; we ll explore this in the sample source.

Let s walk through an example that verifies the push and pop functions of our previous stack example. Cut presents a simple interface that we build to, which is demonstrated in Listing 24.6.

We declare our locals at line 5 and 6 (our two stacks for which we ll perform our tests) and then four functions that serve as the interface to cut.

The first two functions to note are the first and last. These are the initialization function, __CUT_BRINGUP__Explode (at lines 8 “20), and the post test execution function, __CUT_TAKEDOWN__Explode (at lines 74 “85). The cut framework calls the

bringup function prior to test start, and once all of the test functions have been performed, the takedown function is called. Note that we simply perform our necessary initialization (initialize our two stacks with stackCreate ), but we also perform unit testing here to ensure that the required elements are available for us in the unit test. Similarly, in the takedown function, which is called once all unit tests have been called, we destroy our stacks, but we also check the return status of these calls.

The unit tests are encoded as function with a prefix __CUT__ . This allows the cutgen utility to find them and call them within the test framework. As we perform the necessary elements of our unit tests, we call a special function called ASSERT , which is used to log errors. The ASSERT function has two pieces: a test expression and a string emitted if the test fails. The test expression identifies the success condition, and if false, then the test element fails.

Note that in some cases, we have multiple tests for each API element (such as shown for lines 64 “65).

Listing 24.6: Unit Test Example Written for Cut (on the CD-ROM at ./source/ch24/_cut/test_1.c )
start example
  1:  #include <stdio.h>  2:  #include "stack.h"  3:  #include "cut.h"  4:   5:  stack_t myStack_1;  6:  stack_t myStack_2;  7:   8:  void __CUT_BRINGUP__Explode( void )  9:  {  10:  int ret;  11:   12:  printf("Stack test bringup called\n");  13:   14:  ret = stackCreate( &myStack_1, 5 );  15:  ASSERT( (ret == 0), "Stack 1 Creation." );  16:   17:  ret = stackCreate( &myStack_2, 5 );  18:  ASSERT( (ret == 0), "Stack 2 Creation." );  19:   20:  }  21:   22:   23:  void __CUT__PushConsumptionTest( void )  24:  {  25:  int ret;  26:   27:  /* Exhaust the stack */  28:   29:  ret = stackPush( &myStack_1, 1 );  30:  ASSERT( (ret == 0), "Stack Push 1 failed." );  31:   32:  ret = stackPush( &myStack_1, 2 );  33:  ASSERT( (ret == 0), "Stack Push 2 failed." );  34:   35:  ret = stackPush( &myStack_1, 3 );  36:  ASSERT( (ret == 0), "Stack Push 3 failed." );  37:   38:  ret = stackPush( &myStack_1, 4 );  39:  ASSERT( (ret == 0), "Stack Push 4 failed." );  40:   41:  ret = stackPush( &myStack_1, 5 );  42:  ASSERT( (ret == 0), "Stack Push 5 failed." );  43:   44:  ret = stackPush( &myStack_1, 6 );  45:  ASSERT( (ret == -1), "Stack exhaustion failed." );  46:   47:  }  48:   49:   50:  void __CUT__PushPopTest( void )  51:  {  52:  int ret;  53:  int value;  54:   55:  /* Test two pushes and then two pops */  56:   57:  ret = stackPush( &myStack_2, 55 );  58:  ASSERT( (ret == 0), "Stack Push of 55 failed." );  59:   60:  ret = stackPush( &myStack_2, 101 );  61:  ASSERT( (ret == 0), "Stack Push of 101 failed." );  62:   63:  ret = stackPop( &myStack_2, &value );  64:  ASSERT( (ret == 0), "Stack Pop failed." );  65:  ASSERT( (value == 101), "Stack Popped Wrong Value." );  66:   67:  ret = stackPop( &myStack_2, &value );  68:  ASSERT( (ret == 0), "Stack Pop failed." );  69:  ASSERT( (value == 55), "Stack Popped Wrong Value." );  70:   71:  }  72:   73:   74:  void __CUT_TAKEDOWN__Explode( void )  75:  {  76:  int ret;  77:   78:  ret = stackDestroy( &myStack_1 );  79:  ASSERT( (ret == 0), "Stack 1 Destruction." );  80:   81:  ret = stackDestroy( &myStack_2 );  82:  ASSERT( (ret == 0), "Stack 2 Destruction." );  83:   84:  printf( "\n\nTest Complete\n");  85:  } 
end example
 

Now that we have our unit test file (encoded for the cut environment), let s look at how we make this an executable test. While this can be easily encoded in a simple Makefile, we ll demonstrate command-line building for this example.

Three files are needed from the cut system (the URL from which these files can be obtained is provided in the Resources section of this chapter). The cutgen.py utility builds the unit test environment, given our set of unit test source files. This is a Python file, so a Python interpreter will be necessary on the target system. Two other files are  cut.h and  libcut.inc , which are ultimately linked with our unit test application.

The first step is creating the cut unit test environment. This creates C main and brings together the necessary APIs used by the unit tests. The cutget.py utility provides this for us, as demonstrated here:

 cutgen.py test_1.c > cutcheck.c 

Given our unit test file ( Listing 24.6), we provide this as the single argument to cutgen.py and redirect the output into a source file called cutcheck.c . To further understand what cutgen has provided, let s now look at this file (shown in Listing 24.7).

The automatically generated file from cutgen simply brings together the unit tests present in our unit test files and calls them in order. We could provide numerous unit test files to cutgen , which would result in additional unit test functions being invoked within the generated file.

Listing 24.7: Unit Test Environment Source Created By cutgen
start example
  1:  #include "libcut.inc"  2:   3:   4:  extern void __CUT_BRINGUP__Explode( void );  5:  extern void __CUT__PushConsumptionTest( void );  6:  extern void __CUT__PushPopTest( void );  7:  extern void __CUT_TAKEDOWN__Explode( void );  8:   9:   10:  int main( int argc, char *argv[] )  11:  {  12:  cut_init( -1 );  13:   14:  cut_start( "Explode", __CUT_TAKEDOWN__Explode );  15:  __CUT_BRINGUP__Explode();  16:  __CUT__PushConsumptionTest();  17:  __CUT__PushPopTest();  18:  cut_end( "Explode" );  19:  __CUT_TAKEDOWN__Explode();  20:   21:   22:  cut_break_formatting();  23:  return 0;  24:  } 
end example
 

Finally, we simply compile and link the files together to build a unit test image. This implies the automatically generated source file, unit test file, and the source to test (  stack.c ). This is illustrated here:

 # gcc -o cutcheck stack.c test_1.c cutcheck.c 

We can then execute the unit test by simply invoking cutcheck . This will emit numbers and . characters to indicate progress through the unit test process.

 # ./cutcheck    Stack test bringup called            0.........   10.....        Test Complete        # 

The cut system provides some additional features that we ve not addressed, but from this quick review, it s easy to see how powerful and useful this simple utility can be.

Embedded Unit Test

The Embedded Unit Test framework (called Embunit) is an interesting framework that s designed for embedded systems. The framework can operate without the need for standard C libraries and allocates objects from a const area. Embunit also provides a number of tools to generate test templates and also the main function for the test environment.

The Embunit framework is very similar to cut and provides a very useful API for testing. Some of the test functions that are provided in Embunit are shown in Table 24.1.

Table 24.1: Test Functions Provided By Embunit

Function

Purpose

TEST_ASSERT_EQUAL_STRING(exp,actual)

Assert on failed string compare

TEST_ASSERT_EQUAL_INT(exp,actual)

Assert on failed integer compare

TEST_ASSERT_NULL(pointer)

Assert if pointer is NULL

TEST_ASSERT_NOT_NULL(pointer)

Assert if pointer is not NULL

TEST_ASSERT_MESSAGE(cond,message)

Assert and emit message if the condition is false

TEST_ASSERT(condition)

Assert if the condition is false

TEST_FAIL(message)

Fail the test, emitting the message

Let s now look at a unit test coded for the Embunit framework, and then we ll look at the main program that sets up the environment. In Listing 24.8, a minimal unit test for Embunit is shown. At line 1, we include the embUnit header file (which makes the test interface available to our unit test). We then define two functions that are called before and after each unique unit test that s identified to embUnit : setUp at lines 7 “10 and tearDown at lines 13 “16.

We then define our individual unit tests (lines 19 “54). Three tests are illustrated here; the first is called testInit (lines 19 “25), the second is called testPushPop (lines 28 “45), and the third is called testStackDestroy (lines 48 “54). Like our earlier unit tests, we perform an action and then test the result. In this case, we use the Embunit-provided TEST_ASSERT_EQUAL_INT function to check the response, and if the assert fails, an error message is printed.

Finally, we specify the unit tests that are available within the StackTest_tests function (lines 57 “68). This is done in the context of a test fixtures structure. We define fixtures as simply an array that s initialized with the provided functions and function names . Note that we provide our three unit tests here, providing a name for each to indicate the specific test in the event a failure occurs. We then create another structure at lines 64 “65 that defines our test, setup function, teardown function, and fixtures (our list of unit tests). This new structure (called StackTest ) is returned to the caller, which we ll explore shortly.

Listing 24.8: Unit Test Coded for the Embunit Framework (on the CD-ROM at ./source/ch24/emb/stackTest.c )
start example
  1:  #include <embUnit/embUnit.h>  2:  #include <stdio.h>  3:  #include "stack.h"  4:   5:  stack_t myStack;  6:   7:  static void setUp( void )  8:  {  9:  printf("setUp called.\n");  10:  }  11:   12:   13:  static void tearDown( void )  14:  {  15:  printf("tearDown called.\n");  16:  }  17:   18:   19:  static void testInit( void )  20:  {  21:  int ret;  22:   23:  ret = stackCreate( &myStack, 5 );  24:  TEST_ASSERT_EQUAL_INT( 0, ret );  25:  }  26:   27:   28:  static void testPushPop( void )  29:  {  30:  int ret, value;  31:   32:  ret = stackPush( &myStack, 55 );  33:  TEST_ASSERT_EQUAL_INT( 0, ret );  34:   35:  ret = stackPush( &myStack, 101 );  36:  TEST_ASSERT_EQUAL_INT( 0, ret );  37:   38:  ret = stackPop( &myStack, &value );  39:  TEST_ASSERT_EQUAL_INT( 0, ret );  40:  TEST_ASSERT_EQUAL_INT( 101, value );  41:   42:  ret = stackPop( &myStack, &value );  43:  TEST_ASSERT_EQUAL_INT( 0, ret );  44:  TEST_ASSERT_EQUAL_INT( 55, value );  45:  }  46:   47:   48:  static void testStackDestroy( void )  49:  {  50:  int ret;  51:   52:  ret = stackDestroy( &myStack );  53:  TEST_ASSERT_EQUAL_INT( 0, ret );  54:  }  55:   56:   57:  TestRef StackTest_tests( void )  58:  {  59:  EMB_UNIT_TESTFIXTURES( fixtures ) {  60:  new_TestFixture("testInit", testInit ),  61:  new_TestFixture("testPushPop"", testPushPop ),  62:  new_TestFixture("testStackDestroy", testStackDestroy ),  63:  };  64:  EMB_UNIT_TESTCALLER( StackTest, "StackTest",  65:  setUp, tearDown, fixtures );  66:   67:  return( TestRef)&StackTest;  68:  } 
end example
 
Note  

The EMB_UNIT_TESTFIXTURES and EMB_UNIT_TESTCALLER are macros that create special arrays representing the individual unit tests (fixtures) as well as the unit test aggregate ( StackTest ).

Our main program provides the means to invoke the unit tests (see Listing 24.9). We include the embUnit header file to gather the types and symbols. At line 3, we declare our previous function, which creates the text fixtures array (recall from Listing 24.8, lines 57 “68). We call the TestRunner_start function to initialize the test environment and then invoke TestRunner_runTest with our unit test fixture init function ( StackTest_tests ). This invokes all of the unit tests that we discussed from Listing 24.8. When done, we call TestRunner_end , which emits statistics about the unit test, including the number of tests run and the number of failed tests.

Listing 24.9: Embunit Main Program (on the CD-ROM at ./source/ch24/emb/main.c )
start example
  1:  #include <embUnit/embUnit.h>  2:   3:  TestRef StackTest_tests( void );  4:   5:  int main( int argc, const char *argv[] )  6:  {  7:  TestRunner_start();  8:  TestRunner_runTest( StackTest_tests() );  9:  TestRunner_end();  10:  return 0;  11:  } 
end example
 

Building the unit test within Embunit simply involves compiling and linking our source files together with the Embunit library. In order to find the Embunit library, we must specify its location (as well as the location of the header files). Building and running the unit test is illustrated as follows :

 # gcc -Wall -I/usr/local/src/embunit/ \             -L/usr/local/src/embunit/lib \             -o stackTest main.c stack.c stackTest.c  -lembUnit       # ./stackTest       .setUp called.       testInit called       tearDown called.       .setUp called.       tearDown called.       .setUp called.       tearDown called.              OK (3 tests)       # 

Using GCC, we build our image called stackTest and then invoke it. We see that setUp and tearDown are called three times each, before and after each of our unit tests. In the end, we see that the Embunit test environment reports that three tests were run and all were okay.

expect Utility

The expect utility has found wide use in the testing domain. expect is an application that scripts programmed dialogues with interactive programs. Using expect , we can spawn an application and then perform a script consisting of dialogue with the application. This dialogue consists of a series of statements and expected responses. In the test domain, the statements are the stimulus to the application, and the expected response is what we expect from a valid application under test.

The expect utility can even talk to numerous applications at once and has a very rich structure for application test.

Consider the following test in Listing 24.10. At line 3, we set an internal variable of timeout to 1. This tells expect that when the timeout keyword is used, the timeout will represent 1 second (instead of the default of 10 seconds). Next, we declare a procedure called sendexpect . This function provides both the send and expect behaviors in one function. It sends the out string (argument one) to the attached process. The expect function in this case uses the pattern match behavior. Two possibilities exist for what we expect as input from the attached process. If we receive the in string (argument 2 of the sendexpect procedure), then we ve received what we expected and emit a passed message. Otherwise, we wait, and when the timeout occurs, we call it a failure and exit.

At line 14, we spawn our test process (in this case we re testing the bc calculator). We consume the input from bc s startup by expecting the string warranty. We then emit an indicator to stdout , indicating that the test has started (line 17). At this point, we begin our test. Using the sendexpect procedure, we send a command to the application and then provide what we expect as a response. Since we re testing a calculator process, we provide some simple expressions (lines 20 “22), followed by a slightly more complex function example. In the end, we emit the bc quit command, expecting eof (an indication that the bc application terminated ).

Listing 24.10: Testing the bc Calculator with expect (on the CD-ROM at ./source/ch24/expect/test_bc )
start example
  1:  #!/usr/local/bin/expect -f  2:   3:  set timeout 1  4:   5:  proc sendexpect { out in } {  6:  send $out  7:  expect {  8:  $in         { puts "passed\n" }  9:  timeout     { puts "***failed\n" ; exit }  10:  }  11:  }  12:   13:  # Start the bc test  14:  spawn bc  15:  expect "warranty"  16:   17:  puts "Test Starting\n"  18:   19:  # Test some simple math  20:  sendexpect "2+4\n" "6"  21:  sendexpect "2*8\n" "16"  22:  sendexpect "9-2\n" "7"  23:   24:  # Test a simple function  25:  sendexpect \  26:  "define f (x) { if (x<=1) return(1); return(f(x-1) * x); }\n" "\r"  27:  sendexpect "f(5)\n" "120"  28:   29:  # End the test session  30:  sendexpect "quit\n" eof  31:   32:  puts "Test Complete\n"  33:   34:  exit 
end example
 

The expect method differs greatly from our unit test examples, but it is a very powerful mechanism not only for testing but also for automated tasks , even those on remote systems.




GNU/Linux Application Programming
GNU/Linux Application Programming (Programming Series)
ISBN: 1584505680
EAN: 2147483647
Year: 2006
Pages: 203
Authors: M. Tim Jones

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net