Next: , Previous: Memory handling, Up: Top


8 Unit testing support

It is very important testing your classes before you integrate them into a larger part of code. The Unit testing support in ooc is inspired by the Unit 3 testing framework used mainly in Java. If you are not familiar with the topic I recommend some reading about the "test-driven development" approach to catch the basis and importance of unit testing.

8.1 How to create a unit test?

Unit test classes are subclasses of TestCase class, these are called test cases. A test case in ooc is represented by an executable that can be invoked from the operating system or can be run in a simulator in case of microcontrollers.
The test cases have several test methods, that do different tests. These test methods are executed in the order as they are defined in the test method order table.
The test cases could be organized into test suites. The latter means a batch file in ooc that executes the test cases in a sequence.
Its recommended collecting your testable classes in a static directory (*.o in Linux, *.lib in Windows, etc.) to make things simplier. Although this is not necessary, this makes the linking of your testcase more simply, and enables you creating fake classes as needed.


Creating a unit test is easy!
  1. Make a directory in wich you would like to collect your test cases. This directory will be your test suite directory, since it will hold your test case, your makefiles and test siute batch files. Select this directory as your working folder. (For very small projects this practice may not be necessary, you may use the source directory too for your tests.)
  2. Create your test environment with the following command:
              ~$ ooc --testsuite
    

    This creates the necessary makefiles and test suite batch files into this directory. Edit those files according to the comments in them as necessary. If you are working with an IDE, you can configure it to use these makefiles for convenience. It can be used with the automake tools as well under Linux.

  3. Create your test case with the following command:
              ~$ ooc --new MyClassTest --template test
    

    As a naming convention I recommend using "Test" as a tail for your test classes. This helps identify them, but also lets you use the automatic makefile rules in Linux! In Windows you must add this file to the Makefile: edit it according to the comments in it.

  4. Implement the before_class(), before(), after(), after_class() methods in your test file if they are necessary for your test methods. The skeletons can be found in the created file already.
  5. Implement your test methods. Add them to the test method order table. For details see the next sections.
  6. Build your test case with the supplied Makefile.
  7. Run your test case as an executable.
  8. Run your testsuite. Edit your test suite file (suite.sh or suite.bat) as required by the help of the comments in it, and start that script/batch file.

8.2 Writing a unit test

8.2.1 Writing test methods

Implement your test method as a function of the following prototype:

     static void myclasstest_method1( MyTest self );

You can expect that the before() method has been completed succefully before invoking your test method, and the after() method will be invoked after completing your test method.
The test method can throw an Exception or a subclass of it. If the exception is uncaught within the method, the test case repots the test as failed. If the Exception is not caught within your test method, the after() method will not be invoked!


You must put your test method into the test method order table, to let your test case know what methods to invoke! You can do this using the TEST macro:
     static ROM_ALLOC struct TestCaseMethod methods[] =
     {
         TEST(myclasstest_method1),
     	
         {NULL, NULL} /* Do NOT delete this line! */
     };


Important! In ooc your test methods are not independent! This means that if one of your test methods makes changes into any global or test case member variables, or into the environment, the succeeding test methods will inherit those changes. This is a very important difference from Unit 3, where it is garanteed that all test methods starts in a fresh environment.

8.2.2 Assertions

This is why we do the whole staff! :-) Within our test methods we would like to make assertions on the status of our testable classes. For this purpose we can use a group of assertCondition macros. (Don't be confused: these macros do nothing with those ones in the <assert.h> headers!) The assertCondition macros check the given condition, and if the assertion is not met, print an error message identifying the line of code. The execution of code continues in both cases.
A group of assertCondition macros can print messages defined by the user as well. These macros look like assertConditionMsg.
An example for using the assertCondition macros from the ListTest unit test case, testing the list iterator:

     static
     void
     iterator( ListTest self )
     {
         list_append( self->foolist, foo_new() );
     	
         assertTrue( self->foolist->first == list_first( self->foolist ) );
         assertTrue( self->foolist->last == list_last( self->foolist ) );
         assertTrue( list_first( self->foolist ) == list_last( self->foolist ) );
     	
         list_append( self->foolist, foo_new() );
     	
         assertTrue( self->foolist->first == list_first( self->foolist ) );
         assertTrue( self->foolist->last == list_last( self->foolist ) );
         assertFalse( list_first( self->foolist ) == list_last( self->foolist ) );
         assertTrue( list_last( self->foolist ) == list_next( self->foolist, list_first( self->foolist ) ) );
         assertTrue( list_first( self->foolist ) == list_previous( self->foolist, list_last( self->foolist ) ) );
     }

For the complete list of assertCondition macros see the ooc API documentation, or the testcase.h header file.


There is a macro that reports a failure unconditionally, this is the fail (or the failMsg) macro. It prints the error message unconditionally, and continues. It can be used if the condition to be tested is not a simple assertion, and the code runs on a bad path.

8.2.3 Messages

Normally the test cases do not print any message on the screen: they inform you about the actual method by printing its name, but this info will be deleted if the method was executed succesfully. On a Linux or Windows system there won't be any messages left on the display in case of expected results!

     ~$ ./listtest
     ~$

If some assertions fails, you will see a message similar to this one:

     ~$ ./listtest
     [4] ListTest::iterator()          
             Failed:  [listtest.c : 325]
     Test case ListTest failed: 30/1 (methods run/failed)
     ~$

This could be an example, if our ListTest::iterator() method detected an error in the list iterator implementation. Let's see the meaning of the message!

  1. The number in angle brackets shows the sequence number of the method: the failed method was the fourth executed method in the test case.
  2. ListTest::iterator() informs about the name of the method. The part before the period shows the name of the class, the part after the period identifies the name of the failed test method.
  3. The next line says that an assertion in this method failed. If we used an assertConditionMsg assertion, the message would be displayed here.
  4. In the angle brackets the exact location of the failed assertion is displayed: the name of the source code and the line number in the source code. This information helps you find the failed one fast and easily.
  5. The last line is a summary for the test case. It shows how many test methods was run in the test case, and how many of them was failed.
In some cases your test method may throw an exception that is not caught by your method. These exceptions are caught by the TestCase class, displayed as follows, and the code execution continues with the next test method.
     ~$ ./listtest
     [4] ListTest::iterator()          
             Unexpected exception: Exception, code: 1, user code: 0
     ~$

The caught exception was an Exception class with code 1. Since this is a core ooc exception, evaluating the "exception.h" header it can be seen, this is err_out_of_memory, probably our test method run out of memory.

8.2.4 Overriding virtuals

Use the before_class(), before(), after(), after_class() methods to prepare your test case for test methods. Similar to Unit 3: before_class() and after_class() will run only once: when instantiating your test case class, that is a subclass of TestCase.
The before() and after() methods will run for each test method execution. Generally in before() you can prepare the necessary classes for your test methods, while in after() you should destroy them.
You can use assertConditionMsg assertions freely in these virtuals. It might be usefull especially in the after() method to check that the test method did not corrupt the integrity of your test class.
You can throw ecxeptions in before() and before_class() methods, but never throw in after()!
For complicated test cases you can override these methods, since those are virtual methods (but do not do this, if you do not understand exactly how and when they are called. Check the source code if you have any doubt!)
The exexcution order of the virtual methods is:

For each instantiation of your test case:
{
	Your testcase's parents' constructors
	Your testcase's constructor
	For each testcase_run( TestCase ):
	{
		Your test's parents' before_class() methods
		Your test's before_class() method
		For each test method in the test method table:
		{
			Your test's parents' before() methods
			Your test's  before() method
			Yor test method
			Your test's  after() method
			Your test's parents' after() methods
		}
		Your test's after_class() method
		Your test's parents' after_class() methods
	}
	Your testcase's destructor
	Your testcase's parents' destructors
}

8.2.5 Testing exceptions

Test the expected exceptions as follows! Let's check the index overrun behavior of the Vector class:

     try {
         vector_get_item( self->vector, -1 );   /* Should throw an Exception! */
         fail();                                /* Should never be executed! */
     }
     catch_any
         assertTrue( exception_get_error_code( exception ) == err_wrong_position );
     end_try;

8.2.6 Memory leak test

Tests for possible memory leaks is an essential issue in C! Always run your test cases to discover memory leaks, because this helps you make your code robust and reliable!
In ooc the recommended way is using Valgrind (http://valgrind.org), a memory checker (and much more!) tool available under Linux. (For Windows I do not know such a tool, please infrom me if there is a similar possibility.)
To perform a memory check, run your testcase in Valgrind:

     ~$ valgrind --leak-check=yes --quiet ./myclasstest

To minimize your headache, I recommend running only error free test cases under Valgrind! :-) Hunting for assertion failures together with memory problems could be a nightmare!


Note I.: In the Linux standard C library there may be some optimizations that are reported by Valgrind as a possible memory leak, but in fact they are not an error, especially not yours. If you face with this problem, you can suppress them. To supress them, create an empty test case, run it in Valgrind and create a suppress file. For details see the Valgrind documentation: "2.5. Suppressing errors" (http://valgrind.org/docs/manual/manual-core.html#manual-core.suppress).

Note II.: ooc converts the Unix signals (SIGSEGV, SIGFPE) into ooc exceptions. This let us handle and display those problems the same way as other exceptions in ooc. Unfortunately Valgrind overrides these signal handlers, and as a consequence, your test case can not be run under Valgrind if you force emmitting these Unix signals (e.g. intentionally dereferencing a NULL pointer and checking for SegmentationFault exception.)

8.2.7 Unit testing techniques:

There are several best practices in Unit testing. Some of them could be used in ooc as well.

8.2.7.1 Inherited test cases

If you must write many test cases that requires identical preparation or a special environment, then you may consider creating a test case to make this preparation and which could be the parent for your test cases.
ooc TestCase can be inherited!

8.2.7.2 Using fake objects

In this technique you can mimic the behaviour of an object that is not available during the execution of your test case: you simply replace a class with a fake class that behaves identical (or at least as expected), but does not do its job.
Write your fake class in your test suite directory: all the method names must be identical to the original ones! Link your test case as usual, except that you define your fake object file earlier in the parameter line than the library containing your testable classes. The linker will use the symbols in the order they appear in the parameter list: replacing you class with the fake.


In Linux and Windows this is very simple! Just create your fake class with the ooc tool in the suite directory (you may copy it from your source directory). Do not copy the header file, since it should be the original one! As a naming convention the source file name must NOT end with Test, tail it with Fake instead.
Modifiy your code in the fake class. The rest is done by the supplied Makefiles! (In Windows, you must add your file to the Makefile first!)
8.2.7.3 Using mock objects

I have no clue in this moment, how to elaborate this technique in C. :-(

8.2.8 Dependency lookup

For effective unit testing you need a design in wich your classes do not rely on their dependencies. Establishing your classes uncoupled helps you reuse them much easier and lets you implement unit testing. (For more infromation on this topic I recommend some googling on dependency injection, dependency lookup and such topics in Java.) Theoretically it is possible writing a cotainer class in ooc that could handle the object/class dependencies based on an external descriptor file (e.g. XML), like in some Java implementations. Although it would be possible, it will not be feasible: small microcontollers do not have the power to execute effectively. So I recommend using the "dependency lookup" idiom in ooc with a very simple implementation:

  1. In your class's implementation source file define an external function prototype, like this:
              extern void injectMyClass( MyClass self );
    

    This is your injector method for your class.

  2. In your class's constructor call your injector. It is the injector's responsibility to fill the members with the dependencies of your class. Do not set those members in your constructor. It is a good idea to put some check in your code that validates the injected dependencies, at least in the debug version.
              Static
              Void
              MyClass_constructor( MyClass self, void * param )
              {
                 chain_constructor( MyClass, self, NULL );
                 injectMyClass( self );
                 assert( ooc_isInstanceOf( self->settings, Settings ) );
              }
    
  3. Create your injector method in a separate file, let's call it injector.c.
              #include "myclass.h"
              #include "implement/myclass.h"
              #include "settings.h"
              
              void injectMyClass( MyClass self )
              {
                 self->settings = settings_new();
              }
    

    You can inject singleton objects too, if you create them before any call to injectors, or you can use reference counted objects for your dependencies alternatively.
    Implement your other injectors in this file too.

  4. Compile and link together your object files.
  5. Copy your injector file into your unit testing directory, and rewrite it as it is required by your unit tests. Compile and link your test: pay atention to link your test injector, instead of the original one! (The supplied Makefiles will take care.)
    See Using fake objects.
Altough in Java there is only a idiom called "inject while constructing", in ooc it makes sense "inject while initializing". Implement injectors for your initialization methods separately, when it makes sense. (e.g. Injecting virtual functions, like mixins into the virtual table.)
Important! Take care of your design in your classes, that if there is a circular dependency (typically a reference-back) between your classes, this approach will result in an endless loop! Break up the circular dependency, and pass the back reference as a constructor parameter!