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

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

最佳答案

如果你确实使用旧方法,订阅 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

关于c# - 如何正确地对异步套接字连接操作进行单元测试并避免 Thread.Sleep?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56905428/

相关文章:

c - c语言的多线程客户端-服务器聊天应用程序

amazon-web-services - Lambda throttle 场景

c++ - 无法从本地网络外部连接到C++ Linux套接字程序

javascript - 如何避免node.js的异步行为?

c# - 异步等待很少有混淆

c# - vba 可扩展性,com 插件指导?

c# - Linux 下 Mono 的应用程序设置

c# - 对上传文件的 Web API 端点进行单元测试

c# - 在 Visual Studio 2017 中禁用 IDE 警告

python - 套接字.gaierror : [Errno -2] Name or service not known