2

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

PMO1948
  • 2,210
  • 11
  • 34
  • 1
    Your method has two ways to terminate: `while (!token.Iscansellationrequested)` will simply break the loop, and `Task.Delay(500, token)` throws an exception. Choose one thing. I would have just used the `while (true)`. – Alexander Petrov Feb 17 '21 at 12:59
  • 1
    `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. – Nkosi Feb 17 '21 at 13:14
  • @Nkosi this particulat method is the SUT - it has some basic logic that creates the model ( I can easily test that part). Your suggestion seems like a great idea! How could I cancel the token after a known amount of time? – PMO1948 Feb 17 '21 at 13:17

4 Answers4

2

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.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • 1
    I passed a cancellation token that should timeout after say 1800 ms. Then I awaited the method. However, all I got was a `TaskCancelledException`, and it did not even verify that the `send` method was called the proper amount of times – PMO1948 Feb 17 '21 at 15:05
2

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

Athanasios Kataras
  • 25,191
  • 4
  • 32
  • 61
  • Would using `await` help here? From my understanding, the task never stops (until the cancellation token is requested)- so it would run infinitely - and I would be awaiting it forever – PMO1948 Feb 17 '21 at 12:36
  • 2
    I added some information on how to cancel after a period of time. – Athanasios Kataras Feb 17 '21 at 12:39
1

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);
            }
        }
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

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.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • as in, remove the try content to its own method and testing it there? And it is unclear to me what you mean by adding an interface to the poller - it is a method within a class – PMO1948 Feb 17 '21 at 12:33
  • @PMO1948 added an example – JonasH Feb 17 '21 at 12:55