如何正确地对异步套接字连接操作进行单元测试并避免Thread.Sleep?

时间:2019-07-05 14:58:53

标签: c# sockets asynchronous tdd

我有一个名为TcpConnector的类,当与端点的连接成功完成时,将引发一个事件。

这是缩短的实现:

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);
        }
    }
}

这是我正在使用的测试:

[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");
}

由于我的BeginConnect()方法是异步执行的,因此不会阻塞调用者,因此我想到了使用Thread.Sleep()的愚蠢方法。但是,这感觉不对。

所以问题是:一个人如何“适当”测试这种方法?特别是对于正确的超时行为。


我的解决方案

为了完整起见,这就是我的课程和测试的样子,使用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);
    }
}

还有两个xUnit测试示例...

[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>();
}

1 个答案:

答案 0 :(得分:3)

如果您使用旧方法,请订阅EventDispatcher,在此订阅的侦听器的印象中发出等待句柄的信号,让单元测试线程在继续之前等待该信号。

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