Unit Testing for Fun and Profit

Unit Testing

"What is unit testing?" you ask (if you haven't asked this, you can safely skip ahead). Unit testing is the practice of writing many small tests to test different behaviours that you are developing. Ideally, each behaviour should have a corresponding test, and each test should test only one behaviour. Unit tests are really handy things to have around, and the help you:

  • modify your code without fear (see next bullet point)
  • quickly and easily check for regressions
  • keep your code flexible (see previous bullet point)
  • see how writing code that uses whatever you are working on feels
  • debugging changes you've made
  • isolating problems with new code
  • probably other things too...
One of the basic building blocks of a good unit test suite is this handy thing called a 'test fixture'. A test fixture lets you group common code between different unit tests, so that tests remain easy to write. A test fixture is comprised of a few things.
  • 'set up' code, which will be run before each test
  • 'tear down' code, which will be run after each test
  • some variables to be set up and torn down
  • code for the actual tests (this code should use the variables mentioned above)
Hopefully now you can see how useful unit tests are. They are so handy, in fact, that there is an entire programming methodology dedicated to them called Test Driven Development (TDD).

Test Driven Development

"What", you ask "is Test Driven Development??" (if you haven't asked this either, skip ahead again). TDD is a programming methodology that can be summed up by the phrase "test twice, write once". When practicing TDD, a programmer starts development on any new features/behaviour by writing a unit test that will fail. A unit test failure can be as simple as not compiling/linking succesfully. Once the programmer has written a failing unit test, he writes just enough code for that test to pass. Once the programmer has written what he thinks is enough code, he will run the test again. If the test doesn't pass, he hasn't written enough(correct) code, so he modifies what he has already written. This continues until the new test (and any tests that already existed) all run without failure. Now that all the tests are running and passing, the programmer is free to revise the code he has already written, and as long as he runs the tests after each change, he will know whether or not his changes have broken anything.

Now that we all understand unit testing and TDD, let me tell you about my development cycle. I have been following something similar to TDD, but less stringent. I like to write tests for some of the bigger behaviour first, and if they start failing unexpectedly, I will write tests for the smaller behaviours that they depend on. You could call my methodology "Top Down Coding And Testing As Needed", or 'TDCATAN', if you are into acronyms. A funny thing about unit testing is that when you always start by writing tests, you will sometimes find that you have written tests that become obsolete. Sometimes you change your mind about what is the correct behaviour and sometimes features get removed. When that happens, the way to get your tests passing again is by modifying or deleting them :P

Examples

Well, I've already written quite a bit here, without so much as a snippet of code. I'd better do that now! One of the things I've been working on is BLayouts without BViews, but I recently decided to merge the BLayoutItem class I had written to proxy BLayouts with the BLayout class. The tests I had written for the BLayoutLayoutItem class (terrible name, I know :P) were all modified to reflect this, but the test fixture is still named LayoutLayoutItemTest.. (I really should change that!).

The common variables shared between tests are:

	BLayout*	fLayout;
	BView*		fTopView;

The setUp method is run before each test (remember?)
	void setUp()
	{
		fTopView = new BView("test", 0, new BGroupLayout(B_HORIZONTAL));
		fTopView->ResizeTo(200, 200);
		fLayout = new BGroupLayout(B_HORIZONTAL);
		fTopView->GetLayout()->AddItem(fLayout);
		fTopView->GetLayout()->AddView(new BButton("hi"));
	}
A new feature that you an see in action there is the call to AddItem(fLayout). This line is nesting the fLayout variable within the layout that manages fTopView.

The tearDown method is run after each test.
	void tearDown()
	{
		delete fTopView;
	}
That one is pretty straight forward!

Here is an example of one of the test methods for that fixture.
	void TestNestedViewlessLayoutHasRightParent()
	{
		BLayout* secondViewlessLayout = new BGroupLayout(B_VERTICAL);
		fLayout->AddItem(secondViewlessLayout);
		CPPUNIT_ASSERT(secondViewlessLayout->Parent() == fTopView);
	}
There are two new things in this code snippet:
  • the call to BLayout::Parent() returns the BView that items added to this BLayout will reside in.
  • CPPUNIT_ASSERT() is a macro defined by the unit testing library Haiku uses, CppUnit. If the condition inside it is false, this test will fail.

The Grand Finale

Now that you've read this, you are probably a unit testing expert! You can learn more about TDD here, and CppUnit here, and there is a nice intro here. Finally, I've put the full text for LayoutLayoutItemTest and another test up on pastebin: