3

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?

Grrbrr404
  • 1,809
  • 15
  • 17

3 Answers3

7

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.

Community
  • 1
  • 1
jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • I do not understand ... How would you use this Code? – Grrbrr404 Feb 15 '12 at 12:44
  • You need to create a class which uses either `TcpClient` or `Socket` internally and make it implement that interface. – jgauffin Feb 15 '12 at 12:59
  • With this interface, you can use MoQ and even raise the event args. – IAbstract Jan 13 '14 at 17:25
  • Can you provide an example of the test methods? thanks. – Yaobin Then Jan 27 '14 at 02:37
  • "You need to create a class which uses either TcpClient or Socket internally and make it implement that interface." -- which then means you need to test the class that you just wrote. It is non-trivial to use TcpClient to implement the INetwork interface you defined, leaving a gap in the testing. – Sly Gryphon Jan 23 '20 at 03:09
  • I did not state otherwise. However, it's easier to do so and be able to test two classes with distinct responsibilities than one which takes care of everything. Today, I would have made an `async` network abstraction instead. – jgauffin Jan 23 '20 at 06:21
  • 1
    8 Years past since I asked the question. I look back to my younger self and can just think everyone starts as a newb :) Thanks again for the help in 2012, cheers – Grrbrr404 Apr 19 '20 at 11:18
  • Just glad to be able to help. We all have to start somewhere :) – jgauffin Apr 19 '20 at 17:56
6

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.

Anastasiosyal
  • 6,494
  • 6
  • 34
  • 40
0

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

karolgro
  • 181
  • 1
  • 8