unit testing an infinite polling method

Viewed 503

I have a method that is started in a new task when the application starts up, and stops when it ends. The method performs some logic every half a second. I am so lost on how I should test it - any leads?

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);
    }
  }
}

where udpDistributer is a new UdpClient(0) that comes from DI and _distributionEndPoint is an IPEndPoint

Basically I want to check that the logic works and that the right data is sent at the proper interval. How can I do this using xunit?

EDIT

I have gotten as far as this:

   // 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));

However, this causes the test to fail with a TaskCanceledException

4 Answers

You verify that udpDistributer.Send() (which I assume you injected, and if not, do so) is called the appropriate amount of times in the time you require it, and then cancel the token.

This documentation here async-programming-unit-testing-asynchronous-code might be just what you are looking for.

Effectively what it says is to use asynchronous test methods and await the function:

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

Also try to avoid signatres like this private async void StartPoll. It's better to always return Task from asynchronous code.

As part of how to test the calls, I'm also assuming that udpDistributer is an interface to the actual implementation. So you can mock/count/setup the mocked interface, to make sure it works as intended. Using Moq to mock an asynchronous method for a unit test

To make the test stop at some point, you can use the cancellation token cancel-async-tasks-after-a-period-of-time

UdpClient Should be wrapped in an abstraction if possible, StartPoll should return Task.

To test that the desired behavior is achieved, use the token to cancel after a known amount of time and check how many times IUdpClient.Send was invoked

Here is an over simplified example

[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)
            );
    }
}

using an abstracted client

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

that can be mocked and injected into the subject under test

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);
            }
        }
    }
}

I would suggest separating the polling logic from the rest of the logic and test them separately. The poller might be difficult to unit test, but it might be made simple enough that a unit test does not add enough value.

For example, a very simple poller might look like this:

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; }
}

Note that this is not quite equivalent to your poller, since this will raise the elapsed event every interval, regardless of how long the Elapsed event takes to run.

You could then add a interface for your poller that allow it to be mocked for other unit tests. You should then be able to manually trigger the polling event and check that the method does what it is supposed to. In some cases it might also be useful to wrap the DateTime.Now method in an interface to allow it to be mocked.

Related