【问题标题】:How can I develop a testable TcpClient / TcpListener Wrapper如何开发可测试的 TcpClient / TcpListener Wrapper
【发布时间】:2022-04-28 22:11:24
【问题描述】:

我想开发一个可测试的 TcpClient / TcpListener 包装器。我希望能够模拟传入和传出的数据。

我想这样做是因为我有更高层的组件应该对网络消息做出反应。出于测试原因,我想模拟(网络)它们。

有人可以给我一个正确的方向吗?

【问题讨论】:

    标签: c# unit-testing architecture


    【解决方案1】:

    没有。不要模拟 ITcpClient 和 INetworkStream。

    网络层无非就是这样:

    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; }
    }
    

    您使用的是SocketTcpClient 还是NetworkStream 无关紧要。

    更新,如何编写测试

    这里有一些使用流利断言和 NSubstitute 的测试示例。

    正在测试的类:

    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');
            }
        }
    }
    

    最后是测试:

    [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>();
        }
    }
    

    更新(2020-01-23)

    今天我只想做一个异步接口:

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

    为此,您需要使用SocketAwaitable

    【讨论】:

    • 我不明白...您将如何使用此代码?
    • 您需要创建一个在内部使用TcpClientSocket 的类,并使其实现该接口。
    • 有了这个接口,你可以使用MoQ,甚至引发事件参数。
    • 你能提供一个测试方法的例子吗?谢谢。
    • 自从我提出这个问题以来已经过去了 8 年。回顾年轻时的自己,我只能认为每个人都是从新手开始的 :) 再次感谢 2012 年的帮助,干杯
    【解决方案2】:

    您可以使用Decorator Pattern

    • 制作您自己的类,只包装 TcpClient
    • 这个类只是传递对 TcpClient 中函数的调用。
    • 此类的重载构造函数可以接受一个实际的 tcp 客户端实例,如果涉及到创建一个,它会包装该实例。
    • 提取接口,以便您的新类实现接口 ITcpClient
    • 更新所有依赖项以使用新接口 ITcpClient
    • 您的新界面现在是可模拟的,注入您的模拟是适当的并进行测试:)

    对 TcpServer 重复相同的操作。

    【讨论】:

      【解决方案3】:

      我建议不要构建新的抽象层,但可以使用已经存在的 - Stream。并非所有案件都可以这样处理,但值得考虑。

      如果服务采用抽象Stream而不是TcpClient,我们可以在生产代码中提供TcpClient.GetStream(),而对于测试可以是MemoryStreamStringStreamFileStream

      【讨论】:

        猜你喜欢
        • 2012-10-18
        • 2013-06-17
        • 2011-10-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-01-02
        • 1970-01-01
        相关资源
        最近更新 更多