Setup and Teardown, reprise

| 0 Comments
Roy adds a little fine tuning to Brian's advice about avoiding setup and teardown in unit tests. In summary; aim to minimise duplication in your test code...

Roy takes issue with Brian's assertion that a little duplication in your test code is OK. I think the answer is somewhere in between. Roy's point about duplication in the test code leading to fragility of the tests is valid and his suggestions are good but I'd still lean towards readability over duplication when push comes to shove. So, for example, my first test is always a test that simply constructs an object. In this test I always use the real object and mocks of everything that it needs. The test shows how the object is constructed and this is valuable as both a test and as documentation. However, copying and pasting the x lines that are needed to create my CWidget class into the top of each unit test leads to a maintenance nightmare. I tend to prefer to create the object under test on the stack, rather than dynamically within the test so creating a helper function that new's me up a default object doesn't translate. Instead I often derive from the object under test and have my derived class contain, and plug in, all of the required mocks. The derived class does the parameterise from above thing so that the test itself doesn't need to.

An example is probably in order; say we're testing a UDP socket server object and the creation test looks something like this:

void CDatagramSocketServerTest::TestConstruct()
{
   const _tstring functionName = _T("CDatagramSocketServerTest::TestConstruct");
   
   Output(functionName + _T(" - start"));
   
   CMockIOPool ioPool;
   
   const size_t numberOfLocks = 47;
   
   CSharedCriticalSection lockFactory(numberOfLocks);
   
   const size_t poolSize = 10;
   
   CDatagramSocketAllocator socketAllocator(lockFactory, poolSize);
   
   const size_t bufferSize = 1024;
   
   CBufferAllocator bufferAllocator(bufferSize, poolSize);
   
   const unsigned long address = INADDR_ANY;
   const unsigned short port = 5001;
   const size_t listenBacklog = 5;
   
   CDatagramSocketServer server(address, port, listenBacklog, ioPool, socketAllocator, bufferAllocator);
   
   CDatagramSocketServer server2(ioPool, socketAllocator, bufferAllocator);
   
   Output(functionName + _T(" - stop"));
}

That's a lot of setup to create an instance of the server object... In other tests we'll use a simple derived class...
void CDatagramSocketServerTest::TestSetAddressDetails()
{
   const _tstring functionName = _T("CDatagramSocketServerTest::TestSetAddressDetails");
   
   Output(functionName + _T(" - start"));
   
   CTestDatagramSocketServer server;
   
   const unsigned long address = INADDR_ANY;
   const short port = 5001;
   const size_t listenBacklog = 1;
   
   server.SetAddressDetails(address, port, listenBacklog);
   
   server.StartAcceptingConnections();
   
   server.CheckResult(_T("|OnStartAcceptingConnections|RequestRead|HandleOperation|"));
   
   Output(functionName + _T(" - stop"));
}

The derived class, CTestDatagramSocketServer includes all of the mocks and objects that we need to plug into the server to create it and overrides some virtual methods so that they create a simple text log to aid in interaction testing.

The result is an object that makes it easier to test the object under test but it does add some complexity and it does make it slightly harder to see what's going on. I find that deriving a class from the class under test and having the derived class provide a simpler interface often makes it clearer exactly what it is that you're testing. As with most things in programming, sometimes this additional complexity is worth it and other times it isn't...

Leave a comment