Reputation: 4638
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
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