2

My class is using PeriodicTimer . I want to mock its time in unit tests. Is it possible? I can set up the class to have a shorter period but that's not the best practice for unit testing.

Nsubstitute example is preferable, but does not really matter.

Maybe it's possible to make a wrapper, however, ValueTask is a bit more tricky. Maybe I need to dig into IValueTaskSource. But maybe someone has a solution?

The code I want to test:

public class Example : BackgroundService
     {
         private readonly PeriodicTimer _timer;
     
         public Example(IConfiguration configuration)
         {
             _timer = new PeriodicTimer(TimeSpan.Parse(configuration["Interval"]));
         }
         
         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
         {
             while (await _timer.WaitForNextTickAsync(stoppingToken)) // I want to simulate this without waiting for real time
             {
                 // Do something
             }
         }
}
  • 3
    What does your class look like and which part do you want to unit-test? Show some code instead of only explaining the situation, please. – Julian Dec 20 '22 at 19:01
  • Since it is a `sealed` class, it can't be mocked. See this [discussion](https://stackoverflow.com/questions/6484/how-do-you-mock-a-sealed-class) – Peter Dec 20 '22 at 19:11
  • 1
    You are on track thinking about a wrapper. A Mock for the wrapper then should have no problem returning a ValueTask. – Ralf Dec 20 '22 at 20:03
  • @ewerspej Hi, I added an example – Viacheslav Rud' Dec 21 '22 at 12:32
  • Yes, thank you. However, you already received a viable answer so I don't have anything to add right now.. – Julian Dec 21 '22 at 15:00

1 Answers1

4

The wrapper is the way to go, and it should be a dependency for your class.

Given these types

public interface IPeriodicTimer : IDisposable
{
    ValueTask<bool> WaitForNextTickAsync (CancellationToken cancellationToken = default);
}

public sealed class StandardPeriodicTimer : IPeriodicTimer
{
    private PeriodicTimer _actualTimer;

    public StandardPeriodicTimer(TimeSpan timeSpan)
        => _actualTimer = new PeriodicTimer(timeSpan);

    public async ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = default)
        => await _actualTimer.WaitForNextTickAsync(cancellationToken);


    public void Dispose() => _actualTimer.Dispose();
}

public class YourDependentClass
{
    public YourDependentClass(IPeriodicTimer timer) => _timer = timer;
    ...
}

You can initialize a YourDependentClass via newing it up, or via dependency injection in production code.

var dependent = new YourDependentClass(new StandardPeriodicTimer(TimeSpan.FromSeconds(1)));

To support the tests, you can create a stub that simply returns true every time without any delay between.

public sealed class TestPeriodicTimer : IPeriodicTimer
{
    public async ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = default)
        => await Task.FromResult(true);

    public void Dispose()
    {}
}

When you use this stub, we're assuming that somehow the timer stops based on some internal condition in the dependent class.

[Test]
public void TestTheDependentClass()
{
    // arrange
    var timer = new TestPeriodicTimer();
    var dependent = new YourDependentClass(timer);

    // act
    dependent.DoSomething();

    // assert
    // method exits because timer loop is done, timer disposed, etc.
    ...
}

Now, it could get a little trickier if you want the timer to fire a certain number of times, and to run assertions for each tick of the timer. You can either switch from a stub to NSubstitute as you suggest, or carry your stub a step further in its implementation.

But, it gets trickier still. The assertions should be about the state of the dependent after the dependent gets a return value for WaitForNextTickAsync. That means

  • upon the very first call to WaitForNextTickAsync you do nothing
  • on all subsequent calls but the final one you do assertions after the state changes
  • and finally, you have to do assertions when the timer is disposed

I'll make this make sense.

The typical periodic timer is called in a loop. Let's say you call a method on your dependent that has this loop.

while (await _timer.WaitForNextTickAsync())
{
    // some work
    ...
}

Furthermore, let's say in your test code you want to assert on the state of the dependency after each time // some work happens, not before.

And finally, the test run has to be finite. That means it must run a certain number of times, or the dependent must somehow exit the loop early. Let's code for the former problem.

public sealed class TestPeriodicTimer : IPeriodicTimer
{
    private readonly int _numberOfExpectedTicks;
    private int _tickNumber;

    // our list of assertions...
    // we assert for tick 1 on the second call to WaitForNextTickAsync
    // we assert for tick 2 on the third call to WaitForNextTickAsync
    // etc.
    // so let's emulate a list that "starts" at index 2 instead of 0 to
    // avoid some confusing code later
    private readonly List<Action<int, bool>> _assertions = new() { null, null };

    public TestPeriodicTimer(int numberOfTicks) => _numberOfExpectedTicks = numberofTicks.

    // this is how we know what to assert when...
    public void AddAssertionsCallback(Action<int, bool> action)
        => _actions.Add(action);

    public async ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = default)
    {
        // get result for current tick
        var result = await Task.FromResult(_numberOfExpectedTicks == ++_tickNumber);
        
        if (_tickNumber != 1)
        {
            // assert for the prior tick
            var assertedTickNumber = _tickNumber - 1;
            _assertions[_tickNumber](_assertedTickNumber, result);
        }
          return result;
    }    

    public void Dispose()
    {
       if (_disposed) return;
       _disposed = true;

        // assert for the final tick
        _assertions[_tickNumber + 1](_tickNumber, result);
    }

    private bool _disposed;
}

Ok, now we can finally code an expressive test.

[Test]
public void TestThreeTicksOfTheTimerInYourDependentClass()
{
    // arrange
    var timer = new TestPeriodicTimer(3);
    var dependent = new YourDependentClass(timer);

    dependent.AddAssertionsCallback((tickNumber, isFinalTick) =>
    {
        // assertions you expect to be true for tick 1 (actually
        // the second call to WatiForNextTickAsync)
        ...
        // and just to make sure...
        Assert.IsTrue(tickNumber, 1);
        Assert.IsFalse(isFinalTick);
    });

    dependent.AddAssertionsCallback((tickNumber, isFinalTick) =>
    {
        // assertions you expect to be true for tick 2 (actually
        // the third call to WatiForNextTickAsync)
        ...
        // and just to make sure...
        Assert.IsTrue(tickNumber, 2);
        Assert.IsFalse(isFinalTick);
    });

    dependent.AddAssertionsCallback((tickNumber, isFinalTick) =>
    {
        // assertions you expect to be true for tick 3 (actually
        // the call to Dispose(), because Dispose() happens after
        // tick 3's work
        ...
        // and just to make sure...
        Assert.IsTrue(tickNumber, 3);
        Assert.IsTrue(isFinalTick);
    });

    // act
    dependent.DoSomething();
    timer.Dispose();

    // assert
    // place any final assertions here as usual
}

This test says I want exactly 3 ticks to occur with the final (third) tick to of course be the last.

Wow. that was a lot of work. But this gives you a way to

  • not sleep/block during a test
  • test assertions for each and every tick if desired

A further improvement would be to share assertions across ticks, etc.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • Thank you. Seems like a good solution. Sadly the timer itself is not directly mockable :( – Viacheslav Rud' Dec 21 '22 at 12:35
  • Ya. It's not uncommon to wrap non-mockable things though. Another example is creating an `INowProvider` to be able to fake a `DateTime.Now` for time-dependent tests, which can be important for testing, for example, a month-end process and so on. – Kit Dec 21 '22 at 17:31