Как правильно выполнить модульное тестирование операции асинхронного подключения и избежать использования Thread.Sleep? - PullRequest
2 голосов
/ 05 июля 2019

У меня есть класс с именем 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 Ответ

3 голосов
/ 05 июля 2019

Если вы используете старый способ, подпишитесь на EventDispatcher, подайте сигнал ожидания в impl этого подписанного слушателя, дайте потоку модульного теста дождаться этого сигнала, прежде чем продолжить.

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...