jkindwall
jkindwall

Reputation: 3906

How to unit test a method that waits for a response to an asynchronous message

I have a WCF service that sends a message to a remote device via an asynchronous messaging protocol (MQTT), then it has to wait for a response from the device in order to simulate a synchronous operation.

I'm doing this by creating a TaskCompletionSource (and a CancellationTokenSource to handle a timeout), storing it in a ConcurrentDictionary, then returning the TCS.Task.Result once it is set. Meanwhile, when the response comes in, a different method handles the response by looking up the TCS in the dictionary and setting its Result accordingly.

This all seems to work in practice, but I'm running into issues when trying to unit test this method. I'm trying to set up an asynchronous task that waits for the SendMessage method to generate the TCS and add it to the dictionary, then simulates a response by pulling it out of the dictionary and setting the result before the timeout elapses.

For the purposes of the unit test, I'm using a timeout period of 500 ms. I've tried this:

Task.Run(() =>
{
    Thread.Sleep(450);
    ctsDictionary.Values.Single().SetResult(theResponse);
});
MessageResponse response = service.SendMessage(...);

I've also tried this:

MessageResponse response = null;
Parallel.Invoke(
    async () =>
    {
        await Task.Delay(250);
        ctsDictionary.Values.Single().SetResult(theResponse);
    },
    () =>
    {
        response = service.SendMessage(...)
    }
);

Both of these strategies work fine when running just this one unit test or even when running all tests in this Unit Test class.

The problem comes when running all unit tests for the solution (2307 tests in total across a couple dozen UnitTest projects). This test consistently fails with the SendMessage method timing out before the response gets set by the asynchronous task, when part of a "Run All Tests" operation. Presumably this is because the scheduling of the tasks is thrown off by all the other unit tests that are being run in parallel, and the timing doesn't end up working out. I've tried playing around with the delay on the task as well as increasing the timeout period considerably, but I still can't get it to consistently pass when all tests are run.

So how can I fix this? Is there some way I can ensure that the SendMessage call and the task that sets the response are scheduled to run at the exact same time? Or is there some other strategy I can use to ensure the timing works out?

Upvotes: 0

Views: 758

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 457312

then it has to wait for a response from the device in order to simulate a synchronous operation.

That's hokey, man. Just have to say it - keep it async. Not only would it be more natural, it would be easier to unit test!

You can minimize the time SendMessage is waiting by first queueing up the SendMessage and then fast-polling for the request to hit the dictionary. That's as tight as you can get it without changing SendMessage (e.g., making it async):

// Start SendMessage
var sendTask = Task.Run(() => service.SendMessage(...));

// SendMessage may not actually be running yet, so we busy-wait for it.
while (ctsDictionary.Values.Count == 0) ;

// Let SendMessage know it can return.
ctsDictionary.Values.Single().SetResult(theResponse);

// Retrieve the result.
var result = await sendTask;

If you still have problems getting in before the timeout, you'll just have to throttle your unit tests (e.g., SemaphoreSlim).

Again, this would be much easier if SendMessageAsync existed with the semantics that it synchronously adds to the dictionary before it awaits:

// Start SendMessage
var sendTask = service.SendMessageAsync(...);

// Let SendMessage know it can return.
ctsDictionary.Values.Single().SetResult(theResponse);

// Retrieve the result.
var result = await sendTask;

No busy-waiting, no delays, no extra threads. Much cleaner.

Upvotes: 1

Related Questions