How can I develop a testable TcpClient / TcpListener Wrapper

前端 未结 2 491
情话喂你
情话喂你 2021-01-14 07:28

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

相关标签:
2条回答
  • 2021-01-14 07:43

    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.

    0 讨论(0)
  • 2021-01-14 07:58

    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.

    0 讨论(0)
提交回复
热议问题