单元测试无限轮询方法

unit testing an infinite polling method

我有一个方法,当应用程序启动时在新任务中启动,并在它结束时停止。该方法每半秒执行一些逻辑。我不知道该如何测试它 - 有线索吗?

public async Task StartPoll(CancellationToken token)
{
  while(!token.IsCancellationRequested)
  {
    try 
    {
       var dict = dependecy.getDictionary();
       var model = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(dict));
       udpDistributer.Send(model, model.Length, _distributionEndPoint);
    }
    finally
    {
      await Task.Delay(500, Token);
    }
  }
}

其中 udpDistributer 是来自 DI 的 new UdpClient(0)_distributionEndPointIPEndPoint

基本上,我想检查逻辑是否有效,以及是否以正确的时间间隔发送了正确的数据。我如何使用 xunit 执行此操作?

编辑

我已经做到了:

   // arrange
   ...
   cts = new CancellationTokenSource(timeout);

   //act
   await sut.StartPoll(cts.Token);

   // assert
   mockUdpDistributer.Verify(x => x.Send(expectedModel, expectedModel.length,
             It.IsAny<IPEndPoint>()), Times.Exactly(expectedTimes));

但是,这会导致测试失败并显示 TaskCanceledException

您确认 udpDistributer.Send()(我假设您已注入,如果没有,则这样做)在您需要的时间内被调用了适当的次数,然后取消令牌。

我建议将轮询逻辑与其余逻辑分开,并分别进行测试。轮询器可能难以进行单元测试,但它可能足够简单以至于单元测试不会增加足够的价值。

例如,一个非常简单的轮询器可能如下所示:

public class SimplePoller
{
    private Timer timer;
    public SimplePoller(TimeSpan interval)
    {
        timer = new Timer(interval.TotalMilliseconds);
        timer.Elapsed += OnElapsed;
        timer.Start();
    }

    private void OnElapsed(object sender, ElapsedEventArgs e) => Elapsed?.Invoke(this, e);
    public void Dispose() => timer.Dispose();
    public event EventHandler Elapsed;
    public bool Enabled { get => timer.Enabled; set => timer.Enabled = value; }
}

请注意,这并不完全等同于您的轮询器,因为这将在每个时间间隔引发 elapsed 事件,而不管 Elapsed 事件需要多长时间 运行。

然后您可以为您的轮询器添加一个接口,允许它被模拟用于其他单元测试。然后您应该能够手动触发轮询事件并检查该方法是否按照预期进行。在某些情况下,将 DateTime.Now 方法包装在接口中以允许对其进行模拟也可能很有用。

此处的文档 async-programming-unit-testing-asynchronous-code 可能正是您要查找的内容。

实际上它所说的是使用异步测试方法并等待函数:

[TestMethod]
public async Task CorrectlyFailingTest()
{
  await SystemUnderTest.FailAsync();
}

也尽量避免这样的签名 private async void StartPoll。最好始终从异步代码中 return Task

作为如何测试调用的一部分,我还假设 udpDistributer 是实际实现的接口。所以你可以 mock/count/setup 模拟接口,以确保它按预期工作。 Using Moq to mock an asynchronous method for a unit test

要让测试在某个时候停止,您可以使用取消标记 cancel-async-tasks-after-a-period-of-time

UdpClient 应该尽可能包装在抽象中,StartPoll 应该 return Task.

要测试是否实现了所需的行为,请在已知时间后使用令牌取消并检查 IUdpClient.Send 被调用了多少次

这是一个过于简化的例子

[TestFixture]
public class MyTestClass {
    
    [Test]
    public async Task MyTestMethod() {
        //Arrange
        int expectedTimes = 3; 
        var timeout = TimeSpan.FromMilliseconds(1800);
        CancellationTokenSource cts = new CancellationTokenSource();
        var mockUdpDistributer = new  Mock<IUdpClient>();
        var sut = new MyClass(mockUdpDistributer.Object);

        //Act
        Task start = sut.StartPoll(cts.Token);
        await Task.Delay(timeout);
        cts.Cancel();

        //Assert
        mockUdpDistributer
            .Verify(x => 
                x.Send(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<IPEndPoint>())
                , Times.AtLeast(expectedTimes)
            );
    }
}

使用抽象客户端

public interface IUdpClient {
    void Send(byte[] datagram, int bytes, System.Net.IPEndPoint endPoint);
}

可以模拟并注入到被测对象中

class MyClass {
    private readonly IUdpClient udpDistributer;
    private readonly IPEndPoint _distributionEndPoint;

    public MyClass(IUdpClient udpDistributer) {
        this.udpDistributer = udpDistributer;
    }

    public async Task StartPoll(CancellationToken token) {
        while (!token.IsCancellationRequested) {
            try {
                object dict = new object();//<-- dependecy.getDictionary();
                byte[] model = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(dict));
                udpDistributer.Send(model, model.Length, _distributionEndPoint);
            } finally {
                await Task.Delay(500, token);
            }
        }
    }
}