【问题标题】:Unit Test heartbeat functionality单元测试心跳功能
【发布时间】:2018-11-16 08:29:15
【问题描述】:

我有一个基于 C# 编码的 TCP 的客户端-服务器系统。我被要求添加一个双向心跳来发现网络中的任何元素是否冻结并且没有响应。

我通过从服务器向所有连接的客户端发送心跳并等待响应解决了这个问题。如果响应没有按时到达,则会超时,如果服务器在 2 秒内没有发送任何心跳(假设服务器已冻结),则客户端会超时。

一切都在内部发生,服务器和客户端都会公开任何事件(接收心跳、回复心跳,按照请求静默发生)

问题是......如何为这样的功能创建单元测试或集成测试?

注意:我在 Visual Studio、C#、.Net 4.6.1 中编码,使用 NUnit3 进行测试

简单的伪代码示例:

//we have one connector for each connected client
class connector{

    //if the echoReceived timer is not reseted on time it will complain
    Timer echoReceived = new Timer(200ms);
    //Another timer for sending beats
    Timer heartbeatSender = new Timer (1000ms);

    OnClientConnected(Client)
    {        
        echoReceived.elapsed += () => { ShowError("Client did not reply") };

        heartbeatSender.elapsed += () => {
            Send(client, new Message(Heartbeat));
            echoReceived.Enabled = true;
        });
        heartbeatSemder.isEnabled = true;
    }

    OnNewMessageFromClient(Client, message)
    {
        if(message is echoedHeartBeat)
        {
            echoReceived.Enabled= false;
            echoReceived.Reset();
        }        
    }

}

在客户端

class client 
{
     Timer ServerDeadTimeOut = new Timer (1000ms);

     OnStart()
     {
         serverDeadTimeOut.Elapsed += () => { ShowError("Server is dead"); };
         serverDeadTimeOut.isEnabled = true;
     }

     OnNewMessageFromServer(message)
     {
         if(message is HeartBeatMessage)
         {
             serverDeadTimeOut.Reset();
             Send(new HeartBeatEchoMessage);
         }
     }
}

【问题讨论】:

  • 抽象出实现问题,以便更灵活地测试预期行为。

标签: c# unit-testing nunit client-server integration-testing


【解决方案1】:

我会选择单元测试,集成需要在这两者之间建立某种联系,具体取决于实现它可能更容易或更难。单元测试将简单地依赖于抽象。只需将接口注入到连接器中,并为单元测试将它们替换为 Mocks 以模拟您为该类设计的场景。当然,你必须测试 Timer、Logger 和 ClientListener 的实际实现,但这就是单元测试的目的。

public class ConnectorTest
{
    private readonly Connector _connector;
    private readonly TimerMock _receiverMock;
    private readonly TimerMock _senderMock;
    private readonly LoggerMock _loggerMock;
    private readonly ClientListenerMock _listenerMock;

    public void Setup()
    {
        _listenerMock = new ClientListenerMock();
        _receiverMock = new TimerMock();
        _senderMock  = new TimerMock();
        _loggerMock = new LoggerMock();
        _connector = new Connector(_listenerMock, _receiverMock, _senderMock, _loggerMock)
    }

    public void HeartbeatSender_ShouldBeEnabled_OnceClientIsConnected()
    {
        _listenerMock.SimulateClientConnection();

        Assert.IsTrue(_senderMock.IsEnabled);
    }

    public void Error_ShouldBeLogged_WhenConnectedClientDidNotEchoed()
    {
        _listenerMock.SimulateClientConnection();

        _receiverMock.SimulateEchoTimeout();

        Assert.AreEqual("Client did not reply", _loggerMock.RecentErrorMessage);
    }
}

【讨论】:

  • 我想我没有得到你的解决方案。你写了大约 50 行代码(有潜在的错误)来测试 heartbeatSemder.isEnabled = true; 发生了。但是您并不关心是否实际发送了心跳脉冲。只是这条线被调用了。至于第二个..我不知道你如何模拟超时,但我担心你只是调用一个事件所以..心跳系统根本没有测试。
  • 这或多或少只是为了补充我所做的描述。我没有为您的伪代码创建完整的测试解决方案。刚刚提供了如何对其进行单元测试的想法。
  • 但问题是如何对计时器正确、自动重置以及整个心跳解决方案是否有效进行单元测试
  • 单元测试计时器并不容易,因为它们在设计上是异步的,所以使用单元测试是一个非常好的主意(抽象真实的网络连接和所有可能改变测试结果的可重复单元测试规则)。使用一些模拟库,如 moq,重构类来传递抽象(接口),然后模拟计时器或其他与您想要测试的逻辑无关的组件。
【解决方案2】:

我认为如果我们只做几个小改动,对这个类进行单元测试会相当简单。首先,您肯定希望摆脱以下内容:

Timer echoReceived = new Timer(200ms);

在每个单元测试中等待 200 毫秒听起来不是一个好主意。您可以做的是将这 200 毫秒提取到从构造函数初始化的类字段中,以便您可以将其传递较低的值,例如 1 毫秒。但实际上,您可以通过定义一个类似于 ITimer 的接口来使它变得更好,看起来像这样:

interface ITimer 
{
    int Interval { get; }
    event EventHandler OnElapsed;  
    void Reset();
}

这样,在单元测试中,您可以提供一个存根,例如像这样实现的

class StubTimer 
{
   [...]
   void TickNow() 
   {
      OnElapsed.Invoke( ... ) // fire the event, to avoid Thread.Sleep (waiting n miliseconds)
   }
   [...]
}

您可能还应该将 ShowError 函数作为构造函数参数,将其分配给类型为 Action<ErrorArgs> 的属性。然后,您可以像这样对其进行单元测试:

public void WhenClientConnected_ButThereIsNoResponse_ItShouldCallShowError() 
{
   var stubHearbeatTimer = new StubTimer();
   var stubTimeoutTimer = new StubTimer();

   // i'm pretty sure it's possibile mock Actions with moq as well
   var wasSendErrorCalled = false;
   var stubErrorAction = (args) => {
      wasSendErrorCalled = true;
   };

   var sut = new connector(stubHearbeatTimer, stubTimeoutTimer, stubErrorAction );
   sut.OnClientConnected( ..dummyClient..);
   stubTimeoutTimer.TickNow();

   Assert.IsTrue(wasSendErrorCalled);
}

请注意,这只是一个伪解码。希望这能回答您的问题!

【讨论】:

    猜你喜欢
    • 2021-09-07
    • 1970-01-01
    • 1970-01-01
    • 2019-11-24
    • 2017-12-28
    • 1970-01-01
    • 2021-08-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多