Стоит ли тестировать код UDP-сервера, и если да, то как и почему - PullRequest
3 голосов
/ 06 февраля 2010

У меня нет большого опыта в юнит-тестировании. Из того, что я узнал, код должен быть отделен, и я не должен стремиться к тестированию частного кода, только к открытым методам, установщикам и т. Д. И т. Д.

Теперь, я понял некоторые базовые концепции тестирования, но у меня есть проблемы с применением более сложных вещей в этом случае ... Внедрение зависимостей, инверсия управления, фиктивные объекты и т. Д. - пока не могу понять это: (

Прежде чем я перейду к коду, вот вопросы.

  • Что именно я должен попробовать проверить в данном классе?
  • Как я могу выполнить эти тестовые задания?
  • Что-то серьезно не так с дизайном класса, который мешает выполнению тестирования должным образом (или это просто что-то не так, даже вне контекста тестирования)?
  • Какие шаблоны проектирования полезны для тестирования сетевого кода в целом?

Кроме того, я пытался выполнить «сначала пишите тесты, затем пишите код, чтобы тесты проходили успешно», поэтому я написал первые два теста, которые просто создают экземпляр класса и запускают его, но затем, когда сервер смог запустить и принимать пакеты, я не знал, что проверять дальше ...

Хорошо, здесь идет фрагмент кода. (примечание: исходный код разбит на несколько пространств имен, поэтому он может показаться немного не в порядке)

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

Большое спасибо всем, кто нашел время, чтобы пройти через мой вопрос и ответить на него!

Ответы [ 2 ]

4 голосов
/ 06 февраля 2010

Стоит ли тестировать? Абсолютно. Вам нужно спроектировать свой код для тестирования , чтобы сделать это простым. Ваше первое утверждение в значительной степени верно. Итак, некоторые дальнейшие комментарии:

Модульное тестирование - это, в основном, тестирование кода отдельно от тестовых данных и не зависит от внешних систем / серверов и т. Д. Функциональное / интеграционное тестирование затем вводит ваши внешние серверы / базы данных и т. Д. Вы можете использовать внедрение зависимостей, чтобы ввести либо реальную ссылку на внешнюю систему или тестовая (смоделированная) система, реализующая тот же интерфейс, и, следовательно, ваш код становится легко тестируемым.

Итак, в приведенном выше примере вы, вероятно, захотите внедрить источник данных UDP в ваш приемник. Ваш источник данных будет реализовывать определенный интерфейс, а фиктивный (или простой тестовый) источник будет предоставлять разные пакеты для тестирования (например, пустой, содержащий действительные данные, содержащие недействительные данные). Это послужит основой для вашего юнит-теста.

Ваш интеграционный (или функциональный? Я никогда не знаю, как его назвать) тест, возможно, будет запускать тестовый источник данных UDP в одной и той же виртуальной машине для каждого теста и передавать данные через UDP в ваш приемник. , Итак, теперь вы проверили базовую функциональность в отношении различных пакетов с помощью своего модульного теста, и вы тестировали реальную функцию клиент / сервер UDP с помощью интеграционного теста.

Итак, теперь вы проверили передачу / прием вашего пакета, вы можете проверить другие части вашего кода. Ваш приемник UDP подключится к другому компоненту, и здесь вы можете использовать внедрение зависимостей для внедрения приемника UDP в ваш восходящий компонент, или поддельный / тестовый приемник , реализующий тот же интерфейс (и т. Д.).

(Примечание: учитывая, что передача UDP ненадежна, даже внутри хоста, вы должны быть готовы каким-то образом ее обслуживать или признавать, что нередко у вас будут проблемы. Но это проблема, связанная с UDP).

1 голос
/ 06 февраля 2010

Я замечаю некоторые проблемы с дизайном в вашем коде, и я думаю, что эти проблемы также влияют на возможность тестирования этого кода.

  1. Я действительно не понимаю цели класса UDPPacketBuffer. Этот класс не содержит ничего. Он содержит свойство Data для чтения / записи, и я заметил только одно, вероятно, полезное - StringContent. Если вы предполагаете пройти через UDP некоторые пакеты уровня приложения, возможно, вам следует создать соответствующие абстракции для этих пакетов. Кроме того, используя UDP, вы должны создать что-то, что поможет вам собрать все части в одну (потому что вы можете получать части своих пакетов в другом порядке). Кроме того, я не понимаю, почему ваш UDPPacketBuffer содержит IPEndPoint. Вот почему вы не можете протестировать этот класс, потому что для этого класса нет очевидной цели.

  2. Очень сложно протестировать класс, который отправляет и получает данные по сети. Но я замечаю некоторые проблемы с вашей реализацией AsyncUdp.

2.1 Гарантия доставки пакетов отсутствует. Я имею в виду, кто отвечает за надежную доставку пакетов?

2.2 Ther не безопасен для нитей (из-за отсутствия безопасности исключений).

Что произойдет, если метод 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.

...