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 new
ing 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.