【问题标题】:Should I test UDP server code, and if so - why and how?我应该测试 UDP 服务器代码吗?如果是,为什么以及如何测试?
【发布时间】:2010-02-06 10:18:48
【问题描述】:

我没有太多的单元测试经验。从我所学到的,代码应该是解耦的,我不应该努力测试私有代码,只测试公共方法、setter等。

现在,我已经掌握了一些基本的测试概念,但是我在将更高级的东西应用到这个案例中时遇到了麻烦......依赖注入、控制反转、模拟对象等 - 还不能理解它: (

在我开始编写代码之前,这里有一些问题。

  • 在给定的课程中,我究竟应该尝试测试什么?
  • 我怎样才能完成这些测试任务?
  • 类设计是否存在严重问题,导致无法正确完成测试(或者即使在测试环境之外也存在明显错误)?
  • 一般来说,哪些设计模式可用于测试网络代码?

另外,我一直在努力遵守“先编写测试,然后编写代码以使测试通过”,这就是为什么我编写了前两个测试来简单地实例化类并运行它,但是当服务器能够启动时并接受数据包,我不知道接下来要测试什么......

好的,这里是代码 sn-p。 (注意:原始代码被拆分在几个命名空间中,这就是为什么它可能看起来有点乱)

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace MyProject1
{
    /// <summary>
    /// Packet buffer that is sent to/received from connection
    /// </summary>
    public class UDPPacketBuffer
    {
        /// <summary>
        /// Buffer size constant
        /// </summary>
        public const int BUFFER_SIZE = 4096;

        private byte[] _data;

        /// <summary>
        /// Byte array with buffered data
        /// 
        /// DataLength is automatically updated when Data is set
        /// </summary>
        /// <see cref="DataLength"/>
        public byte[] Data { get { return _data; } set { _data = value; DataLength = value.Length; } }

        /// <summary>
        /// Integer with length of buffered data
        /// </summary>
        public int DataLength;

        /// <summary>
        /// Remote end point (IP Address and Port)
        /// </summary>
        public EndPoint RemoteEndPoint;

        /// <summary>
        /// Initializes <see cref="UDPPacketBuffer"/> class
        /// </summary>
        public UDPPacketBuffer()
        {
            // initialize byte array
            this.Data = new byte[BUFFER_SIZE];

            // this will be filled in by the caller (eg. udpSocket.BeginReceiveFrom)
            RemoteEndPoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
        }

        /// <summary>
        /// Returns <see cref="Data"/> as a byte array shortened to <see cref="DataLength"/> number of bytes
        /// </summary>
        public byte[] ByteContent
        {
            get
            {
                if (DataLength > 0)
                {
                    byte[] content = new byte[DataLength];
                    for (int i = 0; i < DataLength; i++)
                        content[i] = Data[i];
                    return content;
                }
                else
                {
                    return Data;
                }
            }
        }

        /// <summary>
        /// Returns <see cref="ByteContent"/> converted to string
        /// </summary>
        public string StringContent { get { return Encoding.ASCII.GetString(ByteContent); } }
    }

    /// <summary>
    /// UDP packet-related event arguments passed when invoking events
    /// </summary>
    /// <example>
    /// This example shows how to use UDPPacketEventArgs class when event is invoked.
    /// <code>
    /// if (PacketSent != null)
    ///     PacketSent(this, new UDPPacketEventArgs(buffer, bytesSent));
    /// </code>
    /// </example>
    public class UDPPacketEventArgs : EventArgs
    {
        /// <summary>
        /// Instance of UDPPacketBuffer, holding current event-related buffer
        /// </summary>
        public UDPPacketBuffer buffer { get; private set; }

        /// <summary>
        /// Number of bytes sent to remote end point
        /// </summary>
        public int sent { get; private set; }

        /// <summary>
        /// Initializes <see cref="buffer"/> only. Used when receiving data.
        /// </summary>
        /// <param name="buffer">Buffer sent to or received from remote endpoint</param>
        public UDPPacketEventArgs(UDPPacketBuffer buffer)
        {
            this.buffer = buffer;
        }

        /// <summary>
        /// Initializes <see cref="buffer"/> and <see cref="sent"/> variables. Used when sending data.
        /// </summary>
        /// <param name="buffer">buffer that has been sent</param>
        /// <param name="sent">number of bytes sent</param>
        public UDPPacketEventArgs(UDPPacketBuffer buffer, int sent)
        {
            this.buffer = buffer;
            this.sent = sent;
        }
    }

    /// <summary>
    /// Asynchronous UDP server
    /// </summary>
    public class AsyncUdp : ServerBase
    {
        private const int _defaultPort = 45112;

        private int _udpPort;

        /// <summary>
        /// Port number on which server should listen
        /// </summary>
        public int udpPort { get { return _udpPort; } private set { _udpPort = value; } }

        // should server listen for broadcasts?
        private bool broadcast = false;

        // server socket
        private Socket udpSocket;

        // the ReaderWriterLock is used solely for the purposes of shutdown (Stop()).
        // since there are potentially many "reader" threads in the internal .NET IOCP
        // thread pool, this is a cheaper synchronization primitive than using
        // a Mutex object.  This allows many UDP socket "reads" concurrently - when
        // Stop() is called, it attempts to obtain a writer lock which will then
        // wait until all outstanding operations are completed before shutting down.
        // this avoids the problem of closing the socket with outstanding operations
        // and trying to catch the inevitable ObjectDisposedException.
        private ReaderWriterLock rwLock = new ReaderWriterLock();

        // number of outstanding operations.  This is a reference count
        // which we use to ensure that the threads exit cleanly. Note that
        // we need this because the threads will potentially still need to process
        // data even after the socket is closed.
        private int rwOperationCount = 0;

        // the all important shutdownFlag.  This is synchronized through the ReaderWriterLock.
        private bool shutdownFlag = true;

        /// <summary>
        /// Returns server running state
        /// </summary>
        public bool IsRunning
        {
            get { return !shutdownFlag; }
        }

        /// <summary>
        /// Initializes UDP server with arbitrary default port
        /// </summary>
        public AsyncUdp()
        {
            this.udpPort = _defaultPort;
        }

        /// <summary>
        /// Initializes UDP server with specified port number
        /// </summary>
        /// <param name="port">Port number for server to listen on</param>
        public AsyncUdp(int port)
        {
            this.udpPort = port;
        }

        /// <summary>
        /// Initializes UDP server with specified port number and broadcast capability
        /// </summary>
        /// <param name="port">Port number for server to listen on</param>
        /// <param name="broadcast">Server will have broadcasting enabled if set to true</param>
        public AsyncUdp(int port, bool broadcast)
        {
            this.udpPort = port;
            this.broadcast = broadcast;
        }

        /// <summary>
        /// Raised when packet is received via UDP socket
        /// </summary>
        public event EventHandler PacketReceived;

        /// <summary>
        /// Raised when packet is sent via UDP socket
        /// </summary>
        public event EventHandler PacketSent;

        /// <summary>
        /// Starts UDP server
        /// </summary>
        public override void Start()
        {
            if (! IsRunning)
            {
                // create and bind the socket
                IPEndPoint ipep = new IPEndPoint(IPAddress.Any, udpPort);
                udpSocket = new Socket(
                    AddressFamily.InterNetwork,
                    SocketType.Dgram,
                    ProtocolType.Udp);
                udpSocket.EnableBroadcast = broadcast;
                // we don't want to receive our own broadcasts, if broadcasting is enabled
                if (broadcast)
                    udpSocket.MulticastLoopback = false;
                udpSocket.Bind(ipep);

                // we're not shutting down, we're starting up
                shutdownFlag = false;

                // kick off an async receive.  The Start() method will return, the
                // actual receives will occur asynchronously and will be caught in
                // AsyncEndRecieve().
                // I experimented with posting multiple AsyncBeginReceives() here in an attempt
                // to "queue up" reads, however I found that it negatively impacted performance.
                AsyncBeginReceive();
            }
        }

        /// <summary>
        /// Stops UDP server, if it is running
        /// </summary>
        public override void Stop()
        {
            if (IsRunning)
            {
                // wait indefinitely for a writer lock.  Once this is called, the .NET runtime
                // will deny any more reader locks, in effect blocking all other send/receive
                // threads.  Once we have the lock, we set shutdownFlag to inform the other
                // threads that the socket is closed.
                rwLock.AcquireWriterLock(-1);
                shutdownFlag = true;
                udpSocket.Close();
                rwLock.ReleaseWriterLock();

                // wait for any pending operations to complete on other
                // threads before exiting.
                while (rwOperationCount > 0)
                    Thread.Sleep(1);
            }
        }

        /// <summary>
        /// Dispose handler for UDP server. Stops the server first if it is still running
        /// </summary>
        public override void Dispose()
        {
            if (IsRunning == true)
                this.Stop();
        }

        private void AsyncBeginReceive()
        {
            // this method actually kicks off the async read on the socket.
            // we aquire a reader lock here to ensure that no other thread
            // is trying to set shutdownFlag and close the socket.
            rwLock.AcquireReaderLock(-1);

            if (!shutdownFlag)
            {
                // increment the count of pending operations
                Interlocked.Increment(ref rwOperationCount);

                // allocate a packet buffer
                UDPPacketBuffer buf = new UDPPacketBuffer();

                try
                {
                    // kick off an async read
                    udpSocket.BeginReceiveFrom(
                        buf.Data,
                        0,
                        UDPPacketBuffer.BUFFER_SIZE,
                        SocketFlags.None,
                        ref buf.RemoteEndPoint,
                        new AsyncCallback(AsyncEndReceive),
                        buf);
                }
                catch (SocketException)
                {
                    // an error occurred, therefore the operation is void.  Decrement the reference count.
                    Interlocked.Decrement(ref rwOperationCount);
                }
            }

            // we're done with the socket for now, release the reader lock.
            rwLock.ReleaseReaderLock();
        }

        private void AsyncEndReceive(IAsyncResult iar)
        {
            // Asynchronous receive operations will complete here through the call
            // to AsyncBeginReceive

            // aquire a reader lock
            rwLock.AcquireReaderLock(-1);

            if (!shutdownFlag)
            {
                // start another receive - this keeps the server going!
                AsyncBeginReceive();

                // get the buffer that was created in AsyncBeginReceive
                // this is the received data
                UDPPacketBuffer buffer = (UDPPacketBuffer)iar.AsyncState;

                try
                {
                    // get the length of data actually read from the socket, store it with the
                    // buffer
                    buffer.DataLength = udpSocket.EndReceiveFrom(iar, ref buffer.RemoteEndPoint);

                    // this operation is now complete, decrement the reference count
                    Interlocked.Decrement(ref rwOperationCount);

                    // we're done with the socket, release the reader lock
                    rwLock.ReleaseReaderLock();

                    // run event PacketReceived(), passing the buffer that
                    // has just been filled from the socket read.
                    if (PacketReceived != null)
                        PacketReceived(this, new UDPPacketEventArgs(buffer));
                }
                catch (SocketException)
                {
                    // an error occurred, therefore the operation is void.  Decrement the reference count.
                    Interlocked.Decrement(ref rwOperationCount);

                    // we're done with the socket for now, release the reader lock.
                    rwLock.ReleaseReaderLock();
                }
            }
            else
            {
                // nothing bad happened, but we are done with the operation
                // decrement the reference count and release the reader lock
                Interlocked.Decrement(ref rwOperationCount);
                rwLock.ReleaseReaderLock();
            }
        }

        /// <summary>
        /// Send packet to remote end point speified in <see cref="UDPPacketBuffer"/>
        /// </summary>
        /// <param name="buf">Packet to send</param>
        public void AsyncBeginSend(UDPPacketBuffer buf)
        {
            // by now you should you get the idea - no further explanation necessary

            rwLock.AcquireReaderLock(-1);

            if (!shutdownFlag)
            {
                try
                {
                    Interlocked.Increment(ref rwOperationCount);
                    udpSocket.BeginSendTo(
                        buf.Data,
                        0,
                        buf.DataLength,
                        SocketFlags.None,
                        buf.RemoteEndPoint,
                        new AsyncCallback(AsyncEndSend),
                        buf);
                }
                catch (SocketException)
                {
                    throw new NotImplementedException();
                }
            }

            rwLock.ReleaseReaderLock();
        }

        private void AsyncEndSend(IAsyncResult iar)
        {
            // by now you should you get the idea - no further explanation necessary

            rwLock.AcquireReaderLock(-1);

            if (!shutdownFlag)
            {
                UDPPacketBuffer buffer = (UDPPacketBuffer)iar.AsyncState;

                try
                {
                    int bytesSent = udpSocket.EndSendTo(iar);

                    // note that in invocation of PacketSent event - we are passing the number
                    // of bytes sent in a separate parameter, since we can't use buffer.DataLength which
                    // is the number of bytes to send (or bytes received depending upon whether this
                    // buffer was part of a send or a receive).
                    if (PacketSent != null)
                        PacketSent(this, new UDPPacketEventArgs(buffer, bytesSent));
                }
                catch (SocketException)
                {
                    throw new NotImplementedException();
                }
            }

            Interlocked.Decrement(ref rwOperationCount);
            rwLock.ReleaseReaderLock();
        }
    }

    /// <summary>
    /// Base class used for all network-oriented servers.
    /// <para>Disposable. All methods are abstract, including Dispose().</para>
    /// </summary>
    /// <example>
    /// This example shows how to inherit from ServerBase class.
    /// <code>
    /// public class SyncTcp : ServerBase {...}
    /// </code>
    /// </example>
    abstract public class ServerBase : IDisposable
    {
        /// <summary>
        /// Starts the server.
        /// </summary>
        abstract public void Start();

        /// <summary>
        /// Stops the server.
        /// </summary>
        abstract public void Stop();

        #region IDisposable Members

        /// <summary>
        /// Cleans up after server.
        /// <para>It usually calls Stop() if server is running.</para>
        /// </summary>
        public abstract void Dispose();

        #endregion
    }
}

“测试代码”如下。

namespace MyProject1
{
    class AsyncUdpTest
    {
        [Fact]
        public void UdpServerInstance()
        {
            AsyncUdp udp = new AsyncUdp();
            Assert.True(udp is AsyncUdp);
            udp.Dispose();
        }

        [Fact]
        public void StartStopUdpServer()
        {
            using (AsyncUdp udp = new AsyncUdp(5000))
            {
                udp.Start();
                Thread.Sleep(5000);
            }
        }


        string udpReceiveMessageSend = "This is a test message";
        byte[] udpReceiveData = new byte[4096];
        bool udpReceivePacketMatches = false;

        [Fact]
        public void UdpServerReceive()
        {

            using (AsyncUdp udp = new AsyncUdp(5000))
            {
                udp.Start();
                udp.PacketReceived += new EventHandler(delegate(object sender, EventArgs e)
                {
                    UDPPacketEventArgs ea = e as UDPPacketEventArgs;
                    if (this.udpReceiveMessageSend.Equals(ea.buffer.StringContent))
                    {
                        udpReceivePacketMatches = true;
                    }
                });
                // wait 20 ms for a socket to be bound etc
                Thread.Sleep(20);

                UdpClient sock = new UdpClient();
                IPEndPoint iep = new IPEndPoint(IPAddress.Loopback, 5000);

                this.udpReceiveData = Encoding.ASCII.GetBytes(this.udpReceiveMessageSend);
                sock.Send(this.udpReceiveData, this.udpReceiveData.Length, iep);
                sock.Close();

                // wait 20 ms for an event to fire, it should be enough
                Thread.Sleep(20);

                Assert.True(udpReceivePacketMatches);
            }
        }
    }
}

注意:代码是c#,测试框架xUnit

非常感谢所有花时间回答我的问题并回答它的人!

【问题讨论】:

    标签: c# networking mocking udp xunit.net


    【解决方案1】:

    你应该测试吗?绝对地。您需要设计您的代码以实现可测试性,以使其变得简单。您的第一个陈述在很大程度上是正确的。所以,一些进一步的cmets:

    单元测试主要是针对测试数据单独测试代码,而不依赖于外部系统/服务器等。功能/集成测试然后引入您的外部服务器/数据库等。您可以使用依赖注入来注入真正的外部系统引用,或实现相同接口的测试(模拟)系统,因此您的代码变得易于测试。

    因此,在上述情况下,您可能希望将 UDP 数据源注入您的接收器。您的数据源将实现特定的接口,而模拟(或简单测试)源将提供不同的数据包进行测试(例如,空、包含有效数据、包含无效数据)。这将构成您的单元测试的基础。

    您的集成(或功能性?我不知道该怎么称呼它)测试可能会在同一 VM 中为每个测试启动一个测试 UDP 数据源,并将数据通过 UDP 泵到您的接收者。所以现在您已经通过单元测试测试了面对不同数据包的基本功能,并且您正在通过集成测试测试实际的 UDP 客户端/服务器功能。

    现在您已经测试了数据包传输/接收,您可以测试代码的更多部分。您的 UDP 接收器将插入另一个组件,在这里您可以使用依赖注入将 UDP 接收器注入您的上游组件,或模拟/测试接收器实现相同的接口(等等)。

    (注意:鉴于 UDP 传输即使在主机内部也是不可靠的,您应该准备好以某种方式应对这种情况,或者接受这种情况,您会遇到问题。但这是 UDP 特有的问题)。

    【讨论】:

    • 所以基本上,作为基本单元测试和 UDP 数据源注入的一部分,我可以(应该?)添加 Start(Socket serverSocket) 重载方法,并在代码中进一步使用该套接字对象。然后在我的测试中为 Start(..) 提供模拟对象,它知道如何响应 Socket.BeginReceiveFrom 和 Socket.BeginSendTo 等方法,对吧?
    • 另外,在常规代码中,在 Start() 方法中,我将创建普通的 Socket 对象并将其传递给重载的 Start(Socket)。这是正确的做法吗?
    • 所以你的第一条评论听起来不错。在常规代码中,我会在外部创建(比如说)Socket,并以完全相同相同的方式将其注入到您的代码中。我会设想你的注入尽可能以相同的方式进行测试和现实生活,否则你正在测试/运行不同的代码路径(无论这些差异多么微不足道)
    • ps。我对 C# 不熟悉,并且模拟 Socket 可能有点太低级了,但原则仍然存在。
    • 我理解你的观点,这很有意义。但是,在这个特定的示例中,如何将套接字创建封装到使用 UDP 服务器的类之外?基本的使用场景是:创建服务器,订阅事件,使用给定的参数运行服务器,最终停止服务器。我应该在 AsyncUdp 服务器类中创建实用程序方法来处理正确 Socket 对象的创建,为套接字对象提供设置器,还是在最终用户类和 AsyncUdp 类之间放置另一个抽象级别?
    【解决方案2】:

    我注意到您的代码中存在一些设计问题,并且我认为这些问题也会影响测试此代码的能力。

    1. 我并不真正理解 UDPPacketBuffer 类的用途。这个类没有封装任何东西。它包含读/写数据属性,我注意到只有一个可能有用的是 StringContent。 如果您假设通过 UDP 一些应用程序级别的数据包,也许您应该为这些数据包创建适当的抽象。此外,使用 UDP,您应该创建一些东西来帮助您将所有部分集中在一起(因为您可以以不同的顺序接收部分数据包)。 另外,我不明白为什么您的 UDPPacketBuffer 包含 IPEndPoint。 这就是为什么你不能测试这个类,因为这个类没有明显的目的。

    2. 很难测试通过网络发送和接收数据的类。但我注意到您的 AsyncUdp 实现存在一些问题。

    2.1 没有数据包传送保证。我的意思是,谁负责可靠的数据包传递?

    2.2 没有线程安全(由于缺乏异常安全性)。

    如果从不同的线程同时调用 Start 方法会发生什么?

    并且,考虑以下代码(来自 Stop 方法):

    rwLock.AcquireWriterLock(-1);
    shutdownFlag = true;
    udpSocket.Close();
    rwLock.ReleaseWriterLock();
    

    如果 updSocket.Close 方法引发异常怎么办?在这种情况下,rwLock 应该保持在获取状态。

    在 AsyncBeginReceive 中:如果 UDPPacketBuffer ctor 抛出异常,或者 udpSocket.BeginReceiveFrom 抛出 SecurityException 或 ArgumentOutOfRangeException 怎么办。

    由于未处理的异常,其他函数也不是线程安全的。

    在这种情况下,您可以创建一些辅助类,用于使用 statemant。 像这样的:

    class ReadLockHelper : IDisposable
    {
      public ReadLockHelper(ReaderWriterLockSlim rwLock)
      {
        rwLock.AcquireReadLock(-1);
        this.rwLock = rwLock;
      }
      public void Dispose()
      {
        rwLock.ReleaseReadLock();
      }
      private ReaderWriterLockSlim rwLock;
    }
    

    在你的方法中使用它:

    using (var l = new ReadLockHelper(rwLock))
    {
      //all other stuff
    }
    

    最后。你应该使用ReaderWriterLockSlim 而不是ReaderWriterLock

    来自 MSDN 的重要说明:

    .NET Framework 有两个读写器锁,ReaderWriterLockSlim 和 ReaderWriterLock。 ReaderWriterLockSlim 推荐用于所有新开发。 ReaderWriterLockSlim 类似于 ReaderWriterLock,但它简化了递归规则以及升级和降级锁定状态的规则。 ReaderWriterLockSlim 避免了许多潜在的死锁情况。此外,ReaderWriterLockSlim 的性能明显优于 ReaderWriterLock。

    【讨论】:

    • 所以,总结一下:当你说某段代码不是线程安全的,你说是不是因为,正如你所指出的,很多段代码没有正确处理异常(或者在全部)?我在线程安全方面也没有太多经验,所以欢迎任何评论。谢谢!
    • 顺便说一句,你提出了一些我以前看不到的优秀观点。在我的设计中 UDPPacketBuffer 类的目的是简单地从可能想要监听 UDP 消息或广播它们的类中抽象出网络级的东西。所以,我怎么看,抽象是必要的,因为可能订阅 AsyncUdp 事件并使用其方法的发送者和接收者类都有数据要发送或接收,并且知道数据应该去哪里,或者想知道它来自哪里.但是,我可能因为一棵树而没有看到森林,所以如果您有想法,请拍摄:)
    • UDP 是无状态协议,众所周知,从本质上讲,它无法判断消息是否已传递(只有套接字知道是否已发送,应处理其异常,正如你所指出的)。此外,我认为 AsyncUdp 类的设计不应干扰接收或发送的数据包,因为最终用户类应该知道如何操作通过 UDP 接收的数据包和数据。我在这里做出了错误的假设或设计选择吗?
    • 1.负责将可靠的数据包传递给另一个类是可以的。 2. 我建议创建更多功能的 MyUdpPacket,其中包含应用程序级别的一些逻辑(我的意思是 en.wikipedia.org/wiki/Application_Layer 来自 en.wikipedia.org/wiki/OSI_model)。但是,如果您尝试创建更通用的解决方案以通过 UDP 进行通信,则不应这样做。 3. 是的,我的意思是由于缺乏异常安全性,您可能会遇到微妙的问题(例如,死锁)。在这种情况下,像 ReadLockHelper 这样的东西可以帮助你(或者你可以手动使用 try/finally 块)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-07
    • 2018-08-26
    • 2012-07-29
    • 2021-04-06
    • 1970-01-01
    • 2021-11-21
    • 1970-01-01
    相关资源
    最近更新 更多