我有一个基于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);
}
}
}
我会去单元测试,集成需要在这两者之间建立某种连接,取决于实现它可以更容易或更难。单元测试只依赖于抽象。只需将接口注入连接器,然后进行单元测试,用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);
}
}
我认为如果我们只进行一些小改动,那么对这个类进行单元测试会非常简单。首先,你肯定想要摆脱以下几点:
Timer echoReceived = new Timer(200ms);
在每个单元测试中等待200ms并不是一个好主意。您可以做的是将此200 ms提取到从构造函数初始化的类字段中,以便您可以将其传递给较低的值,例如1ms。但实际上,您可以通过定义像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);
}
请注意,它只是一个伪代码。希望这能回答你的问题!