Testing synchronous communications

| 0 Comments

Today I'm doing some work for a client that involves writing some blocking sockets code to talk to one of our servers. Blocking mode fits well with the architecture of the client application they use; we're moving the app from blocking reads on a serial port to blocking reads on a socket and jiggling the protocol it uses a bit.

Testing blocking calls is actually a bit harder than testing non blocking calls because hand cranking the mock objects so that your object under test works correctly is harder to do when your test harness is blocked in a call to read() and wont come back to you until the call times out (and the operation that you're trying to test fails) or the read completes...

Since we're actually testing the code that adapts the application from one that talks to modems directly to one that talks to them over a socket connection we didn't think that there was much point in additional indirection just for testing; after all, this layer IS the interface that we'd like to mock up. Eventually you get to the point where you have to just write real code that does real things, so we're pretty much stuck talking directly to a real live connected socket and mocking up the remote end so that we can drive the application code with our mock server.

We already had tests and mocks for the server side code from when we were developing that. It was no problem for the test to create a mocked up server as a real socket server, run it up, let it listen for connections, and have the client code connect to it within the test. The problem was that the client used blocking calls and we were controlling the asynchronous server from within the test code by using a mock IOPool that allows us to programmatically decide when to let IO events complete. Unfortunately the client operates like this: Send data to the server, block on reading the response, process the response, return to caller. When it got to the block on reading the response part the test stopped, forever.

We could have run the test server with a real IOPool, one with a real IO completion port inside that just runs, asynchronously, in the background, dealing with requests. We didn't want to do that because it could make some of the tests unreliable where we wanted to single step the server to test various pieces. What we really wanted to be able to say was "process these 7 events, respond this way, then stop, now single step these events". This would allow us to pre-prime the server with responses to the request we were about to test, set it up to run for exactly the number of IO completions that we knew were required to complete the test and then call the blocking client code and have the server run to enable the call to complete. We added a thread to the mock server, a couple of events, a semaphore, a method that allowed us to tell it to process X events and a method that allowed us to wait for it to have finished processing those events and that let us know if it didn't process all of them, or if there were more events waiting when it finished.

That allowed us to write a test like this:

   const _tstring functionName = _T("CSocketModemTest::TestDialModem");
   
   Output(functionName + _T(" - start"));

   const string initString = "initString";
   const string answerString = "answerString";

   const size_t retries = 3;
   const size_t waitConnect = 1;

   const unsigned short serverPort = GetAvailablePort();

   CMockModemConnectionManager connectionManager;

   connectionManager.AddResponse("\r\nOK\r\n");
   connectionManager.AddResponse("\r\nOK\r\n");
   connectionManager.AddResponse("CONNECT 9600\r\n");

   CTestPollingEngineClientManager manager(serverPort, connectionManager);

   const string serverAddress = "127.0.0.1";

   {
      CSocketModem connection(
         initString,
         answerString,
         retries,
         waitConnect,
         serverAddress,
         serverPort);

      const short comPort = 1;

      const char pollingMode = '?';

      connection.Initialise(comPort, pollingMode);

      THROW_ON_FAILURE(functionName, true == manager.WaitForAccept(1000));
   
      manager.CheckResult(_T("|OnConnectionEstablished|RequestRead|FilterReadRequest|HandleOperation|"));
   
      manager.ProcessEventsAsync(11);

      connection.Dial("123");

      THROW_ON_FAILURE(functionName, true == manager.AllAsyncEventsProcessed(1000));

      THROW_ON_FAILURE(functionName, 3 == connectionManager.ResponsesUsed());

      manager.ClearLog();

      connectionManager.CheckResult(_T("|Connect: MODEM|Write: 4 bytes: ATZ.|Read|Write: 11 bytes: initString.|Read|Write: 9 bytes: ATDT 123.|Read|"));

      manager.ProcessEventsAsync(2);
   }

   THROW_ON_FAILURE(functionName, true == manager.AllAsyncEventsProcessed(1000));

   manager.CheckResult(_T("|HandleOperation|FilterReadCompleted|ReadCompleted|RequestWrite|FilterWriteRequest|HandleOperation|HandleOperation|FilterWriteCompleted|WriteCompleted|FilterSocketShutdown: ShutdownSend|OnConnectionClosing|FilterSocketClose|OnConnectionClosed|ReleaseSocket|OnSocketReleased|"));
   
   THROW_ON_FAILURE(functionName, true == manager.WaitForConnectionClose(1000));

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

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

Leave a comment