So, what do these tests look like then?

| 0 Comments

After breaking the socket server into more manageable chunks I started writing the tests. CAsyncSocketConnectionManager is pretty easy to test, it only has one public function; Connect().

So, what exactly do these tests look like?

I don't use a unit testing framework at present, I haven't found a need. Whilst NUnit and JUnit are great I don't feel that the C++ version adds much as C++ doesn't support reflection and that's where these frameworks really seem to win. So my tests are just C++ code that's written in a standardised way... The test to make sure that the Connect() function works currently looks like this:

void CAsyncSocketConnectionManagerTest::TestConnect()
{
   const _tstring functionName = _T("CAsyncSocketConnectionManagerTest::TestConnect");
   
   Output(functionName + _T(" - start"));

   CMockIOPool ioPool;

   CSharedCriticalSection lockFactory(47);

   CAsyncSocketAllocator socketAllocator(lockFactory, 10, 0, false);

   CBufferAllocator bufferAllocator(1024, 10);

   const bool postZeroByteReads = false;

   CLoggingAsyncSocketConnectionManager manager(ioPool, socketAllocator, bufferAllocator, postZeroByteReads);

   CTestSocketServer server;

   InternetAddress address(_T("localhost"), server.GetPort());

   CAsyncSocket *pSocket = manager.Connect(address);

   THROW_ON_FAILURE(functionName, true == server.WaitForAccept(1000));

   ioPool.CheckResult(_T("|AssociateDevice|"));

   manager.CheckResult(_T("|OnOutgoingConnectionEstablished|"));

   pSocket->Release();

   manager.CheckResult(_T("|OnConnectionClosing|OnConnectionClosed|ReleaseSocket|OnSocketReleased|"));

   Output(functionName + _T(" - stop"));
}

This test needs a lot of scaffolding to work, but then it's testing something that's relatively non-trivial...

The actual object under test is the CLoggingAsyncSocketConnectionManager this is a class derived from the real object that we're interested in CAsyncSocketConnectionManager. The reason for deriving a class for the test is that we are interested in the virtual methods that get called during connection creation and tear down. The way the socket server code works is that you're expected to derive from the server and override virtual functions to deal with events. We simply log the name of the function called and any useful information that gets passed to us. To do this we also derive from the CTestLog class which provides the logging and the CheckResult() function.

We use a mock IOPool object because we don't want the hassle of dealing with a multi-threaded IOCP based IO pool whilst we're testing the object under test. The mock IOPool is pretty simple; it also uses CTestLog to provide us with an audit trail of functions called.

The other objects that we need to pass to our object under test are the real deal.

Since we're testing a socket connection we need something to connect to. It would be easy enough to use the local machine's echo server (port 7) or anything else we happen to find when running netstat but doing that would mean that the test could fail if it's run on a machine where that server isn't running... Instead we have a bare bones, synchronous, socket server that we can use for testing. This server CTestSocketServer automatically listens on a port when you create it. Because we dont necessarilly know for sure of a port that will be available it starts at 5001 and keeps walking up the range of ports, one at a time, until it can listen. We need to know the port that we need to connect to so we ask the server by calling GetPort() when we build the address to connect to.

So, the test itself constructs the object under test, constructs a server to listen for connections, works out where that server is listening, calls Connect(), checks that the connection is made and then checks the audit trails of the various objects to make sure the correct things happen...

Who said testing was hard ;)

Most of the code in the test above is simply setting the scene. When we're testing the CAsyncSocket class we need to repeat this setup over and over again so we've created a composite object that handles all of the details we're not interested in... Testing the Read() function of the socket requires some code like this:

void CAsyncSocketTest::TestRead()
{
#pragma TODO("We dont actually check the bytes arrive, this is due to having to read multiple times to get the data")
#pragma TODO("Need a test that does this")

   const _tstring functionName = _T("CAsyncSocketTest::TestRead");
   
   Output(functionName + _T(" - start"));

   CAsyncSocketTestManager manager;

   CAsyncSocket *pSocket = manager.Connect();

   pSocket->Read();

   manager.CheckManagerResult(_T("|RequestRead|AllocateBuffer|HandleOperation|FilterReadRequest|"));

   manager.Write("MESSAGE");

   manager.ProcessEvent();

   manager.CheckManagerResult(_T("|HandleOperation|FilterReadCompleted|ReadCompleted|"));

// with buffer

   CBuffer *pBuffer = manager.AllocateBuffer();

   pSocket->Read(pBuffer);

   pBuffer->Release();

   manager.CheckManagerResult(_T("|RequestRead|HandleOperation|FilterReadRequest|"));

   manager.Write("MESSAGE");

   manager.ProcessEvent();

   manager.CheckManagerResult(_T("|HandleOperation|FilterReadCompleted|ReadCompleted|"));

   pSocket->Release();

   manager.CheckManagerResult(_T("|OnConnectionClosing|OnConnectionClosed|ReleaseSocket|OnSocketReleased|"));

   Output(functionName + _T(" - stop"));
}

The CAsyncSocketTestManager creates an instance of the CAsyncSocketConnectionManager along with all the objects that it needs, and creates a test server to connect to. We can then concentrate on testing the socket that is returned from the call to Connect() rather than being distracted by all the scaffolding that's required to get there...

This test demonstrates another aspect of the mock IOPool that we've created. In normal use the async socket is associated with an IOCP in the IOPool by the connection manager. When reads or writes occur they occur asynchrounously and complete immediately. When the actual read and write have finished a completion packet is queued to the IOCP and processed by the threads in the IOPool... Hard to test? Not at all...

The mock IOPool contains an IOCP but it doesn't spin up any worker threads. Instead we can choose to process a completion packet by calling ProcessEvent(). This function attempts to pull a completion packet out of the IOCP and if it does then it passes control to the handler in exactly the same way that the real IOPool would. The handler is what might be called the 'per socket' data structure by some, the code looks like this:

bool CMockIOPool::ProcessEvent(
   const DWORD timeoutMillis)
{
   DWORD dwIoSize = 0;
   IHandler *pHandler = 0;
   CBuffer *pBuffer = 0;

   bool ok = m_iocp.GetStatus(
      (PDWORD_PTR)&pHandler, 
      &dwIoSize, 
      (OVERLAPPED**)&pBuffer, 
      timeoutMillis);

   if (ok)
   {
      if (pHandler)
      {
         pHandler->HandleOperation(pBuffer, dwIoSize, 0);
      }
   }

   return ok;
}

The handler, the socket, passes the event on to the connection manager to handle and we get details of the event via the virtual functions in the connection manager. So, to test that an async read via an IOCP actually causes the correct code to be called we simply issue it, manage the IOCP carefully from within the test harness and synchronously dispatch the "async" event to the connection manager. Once again we use the audit logs produced by the CTestLog class to make sure the right things happen.

Notice that we're not actually testing that the data that is written to the socket is actually read out correctly, that's work for another test...

Leave a comment