Matthias Güntert
Matthias Güntert

Reputation: 4638

How to properly unit test an async socket connect operation and avoid Thread.Sleep?

I have a class called TcpConnector that raises an event when a connection to an endpoint successfully got completed.

This is the shortened implementation:

public class TcpConnectorEventArgs : EventArgs
{
    public Exception EventException { get; set; }
    [...]
}

public class TcpConnector
{
    public event EventHandler<TcpConnectorEventArgs> EventDispatcher;

    public void BeginConnect(IPEndPoint endpoint, int timeoutMillis)
    {
        var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        var ipcState = new IpcState()
        {
            IpcSocket = socket,
            IpcEndpoint = endpoint,
            IpcTimeoutMillis = timeoutMillis
        };

        try
        {
            ipcState.IpcSocket.BeginConnect(ipcState.IpcEndpoint, HandleConnect, ipcState);
        }
        catch (Exception ex)
        {
            var tcpConnectorEventArgs = new TcpConnectorEventArgs()
            {
                EventSocket = ipcState.IpcSocket,
                EventEndPoint = ipcState.IpcEndpoint,
                EventType = TcpConnectorEventTypes.EventConnectFailure,
                EventException = ex
            };

            EventDispatcher?.Invoke(this, tcpConnectorEventArgs);
        }
    }

    private void HandleConnect(IAsyncResult asyncResult)
    {
        var ipcState = asyncResult.AsyncState as IpcState;

        if (ipcState == null)
        {
            return;
        }

        try
        {
            var result = asyncResult.AsyncWaitHandle.WaitOne(ipcState.IpcTimeoutMillis, true);

            if (result)
            {
                ipcState.IpcSocket.EndConnect(asyncResult);

                var tcpConnectorEventArgs = new TcpConnectorEventArgs()
                {
                    EventSocket = ipcState.IpcSocket,
                    EventEndPoint = ipcState.IpcEndpoint,
                    EventType = TcpConnectorEventTypes.EventConnectSuccess
                };

                // Raise event with details
                EventDispatcher?.Invoke(this, tcpConnectorEventArgs);

                // Check cancellation flag if any subscriber wants the
                // connection canceled
                if (tcpConnectorEventArgs.EventCancel)
                {
                    ipcState.IpcSocket.Close();
                }
            }
            else
            {
                var tcpConnectorEventArgs = new TcpConnectorEventArgs()
                {
                    EventSocket = ipcState.IpcSocket,
                    EventEndPoint = ipcState.IpcEndpoint,
                    EventType = TcpConnectorEventTypes.EventConnectFailure,
                    EventException = new SocketException(10060) // Connection timed out
                };

                // Raise event with details about error 
                EventDispatcher?.Invoke(this, tcpConnectorEventArgs);
            }
        }
        catch (Exception ex)
        {
            var tcpConnectorEventArgs = new TcpConnectorEventArgs()
            {
                EventSocket = ipcState.IpcSocket,
                EventEndPoint = ipcState.IpcEndpoint,
                EventType = TcpConnectorEventTypes.EventConnectFailure,
                EventException = ex
            };

            // Raise event with details about error 
            EventDispatcher?.Invoke(this, tcpConnectorEventArgs);
        }
    }
}

This is the test I am using:

[Fact]
[Trait(TraitKey.Category, TraitValue.UnitTest)]
public void Should_Raise_Event_And_Fail_To_Connect()
{
    // Arrange
    var receivedEvents = new List<TcpConnectorEventArgs>();
    var nonListeningPort = 82;
    var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), nonListeningPort);
    var timeout = 1 * 1000;

    var client = new TcpConnector();
    client.EventDispatcher += (o, e) => receivedEvents.Add(e);

    // Act
    client.BeginConnect(endPoint, timeout);
    Thread.Sleep(10 * 1000);

    // Assert
    receivedEvents.Should().HaveCount(1);
    receivedEvents[0].EventType.Should().Be(TcpConnectorEventTypes.EventConnectFailure);
    receivedEvents[0].EventException.Message.Should().Be("No connection could be made because the target machine actively refused it");
}

As my BeginConnect() method is executing asynchronously and therefor doesn't block the caller, I came up with the goofy approach of using Thread.Sleep(). This however feels wrong.

So the question is: How would one 'properly' test this method? Especially for correct timeout behavior.


My solution

For the sake of completness, this is what my class and test now looks like, using ConnectAsync()

public class TcpConnector
{
    private Socket socket;

    //[...]

    public async Task ConnectAsync(IPEndPoint endpoint)
    {
        this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        await this.socket.ConnectAsync(endpoint);
    }
}

And two example xUnit tests...

[Fact]
[Trait("Category", "UnitTest")]
public async Task Should_Successfully_ConnectAsync()
{
    // Arrange
    var client = new TcpConnector();
    var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);

    // Act
    var connectTask = client.ConnectAsync(endpoint);
    await connectTask;

    // Assert
    connectTask.IsCompletedSuccessfully.Should().BeTrue();
    connectTask.Exception.Should().BeNull();
    client.IsConnected().Should().BeTrue();
}

[Fact]
[Trait("Category", "UnitTest")]
public async Task Should_Throw_Exception_If_Port_Unreachable()
{
    // Arrange
    var client = new TcpConnector();
    var nonListeningPort = 81;
    var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), nonListeningPort);

    // Act & Assert
    var connectTask = client.ConnectAsync(endpoint);
    Func<Task> func = async () => { await connectTask; };

    func.Should().Throw<Exception>();
}

Upvotes: 1

Views: 913

Answers (1)

user11523568
user11523568

Reputation:

If you do use the old way, subscribe to EventDispatcher, signal a wait handle in the impl of this subscribed listener, let the unit test thread wait on this signal before continueing.

var signal = new ManualResetEventSlim(false);
var client = new TcpConnector();
client.EventDispatcher += (o, e) => signal.Set();

client.BeginConnect(endPoint, timeout);

signal.WaitOne(); // consider using an overload that takes a timeout

Upvotes: 3

Related Questions