Grrbrr404
Grrbrr404

Reputation: 1805

How can I develop a testable TcpClient / TcpListener Wrapper

i want to develop a testable TcpClient / TcpListener wrapper. I want to be able to mock the incoming and outgoing data.

I want to do this because i have higher tier components that should react on network messages. For testing reasons i want to mock (the network) them.

Can some one please give me a kick in the right direction?

Upvotes: 3

Views: 4855

Answers (3)

karolgro
karolgro

Reputation: 393

I'd suggest not building new abstraction layer, but is possible using already existing - Stream. Not every case can be handled that way, but it's worth considering.

If the service takes abstract Stream instead of TcpClient, we can provide in production code TcpClient.GetStream(), while for tests it can be MemoryStream, StringStream or FileStream

Upvotes: 0

jgauffin
jgauffin

Reputation: 101130

No. Don't mock ITcpClient and INetworkStream.

A network layer is nothing more than this:

public interface INetworkClient : IDisposable
{
    event EventHandler<ReceivedEventArgs> BufferReceived;
    event EventHandler Disconnected;
    void Send(byte[] buffer, int offset, int count);
}

public class ReceivedEventArgs : EventArgs
{
    public ReceivedEventArgs(byte[] buffer)
    {
        if (buffer == null) throw new ArgumentNullException("buffer");
        Buffer = buffer;
        Offset = 0;
        Count = buffer.Length;
    }

    public byte[] Buffer { get; private set; }
    public int Offset { get; private set; }
    public int Count { get; private set; }
}

It should not matter if you are using a Socket, TcpClient or a NetworkStream.

Update, how to write tests

Here are some test examples using fluent assertions and NSubstitute.

Class being tested:

public class ReceivedMessageEventArgs : EventArgs
{
    public ReceivedMessageEventArgs(string message)
    {
        if (message == null) throw new ArgumentNullException("message");
        Message = message;
    }

    public string Message { get; private set; }
}

public class SomeService
{
    private readonly INetworkClient _networkClient;
    private string _buffer;

    public SomeService(INetworkClient networkClient)
    {
        if (networkClient == null) throw new ArgumentNullException("networkClient");
        _networkClient = networkClient;
        _networkClient.Disconnected += OnDisconnect;
        _networkClient.BufferReceived += OnBufferReceived;
        Connected = true;
    }

    public bool Connected { get; private set; }

    public event EventHandler<ReceivedMessageEventArgs> MessageReceived = delegate { };

    public void Send(string msg)
    {
        if (msg == null) throw new ArgumentNullException("msg");
        if (Connected == false)
            throw new InvalidOperationException("Not connected");

        var buffer = Encoding.ASCII.GetBytes(msg + "\n");
        _networkClient.Send(buffer, 0, buffer.Length);
    }

    private void OnDisconnect(object sender, EventArgs e)
    {
        Connected = false;
        _buffer = "";
    }

    private void OnBufferReceived(object sender, ReceivedEventArgs e)
    {
        _buffer += Encoding.ASCII.GetString(e.Buffer, e.Offset, e.Count);
        var pos = _buffer.IndexOf('\n');
        while (pos > -1)
        {
            var msg = _buffer.Substring(0, pos);
            MessageReceived(this, new ReceivedMessageEventArgs(msg));

            _buffer = _buffer.Remove(0, pos + 1);
            pos = _buffer.IndexOf('\n');
        }
    }
}

And finally the tests:

[TestClass]
public class SomeServiceTests
{
    [TestMethod]
    public void service_triggers_msg_event_when_a_complete_message_is_recieved()
    {
        var client = Substitute.For<INetworkClient>();
        var expected = "Hello world";
        var e = new ReceivedEventArgs(Encoding.ASCII.GetBytes(expected + "\n"));
        var actual = "";

        var sut = new SomeService(client);
        sut.MessageReceived += (sender, args) => actual = args.Message;
        client.BufferReceived += Raise.EventWith(e);

        actual.Should().Be(expected);
    }

    [TestMethod]
    public void Send_should_invoke_Send_of_networkclient()
    {
        var client = Substitute.For<INetworkClient>();
        var msg = "Hello world";

        var sut = new SomeService(client);
        sut.Send(msg);

        client.Received().Send(Arg.Any<byte[]>(), 0, msg.Length + 1);
    }

    [TestMethod]
    public void Send_is_not_allowed_while_disconnected()
    {
        var client = Substitute.For<INetworkClient>();
        var msg = "Hello world";

        var sut = new SomeService(client);
        client.Disconnected += Raise.Event();
        Action actual = () => sut.Send(msg);

        actual.ShouldThrow<InvalidOperationException>();
    }
}

Update (2020-01-23)

Today I would just have made an async interface:

public interface INetworkClient : IDisposable
{
    Task SendAsync(byte[] buffer, int offset, int count);
    Task<int> ReceiveAsync(byte[] buffer, int offset, int count);
}

To achieve that, you need to use a SocketAwaitable.

Upvotes: 7

Anastasiosyal
Anastasiosyal

Reputation: 6626

You could use the Decorator Pattern

  • Make your very own class that just wraps the TcpClient
  • This class simply does pass through calls to functions in TcpClient.
  • An overloaded constructor to this class could accept an instance of an actual tcp client which it would wrap, if creating one is involved.
  • Extract the interface so your new class should implement the interface ITcpClient
  • Update all your dependencies to use the new interface ITcpClient
  • Your new interface is now mockable, inject your mocks were appropriate and test away :)

Repeat the same for TcpServer.

Upvotes: 6

Related Questions