Reputation: 123
Using NSubstitute, I've encountered what seems to be either a bug or me not understanding how NSubstitute or async/await works.
I'm trying to test an async method which has a while loop like so:
public class ClassUnderTest {
private readonly IDependencyToBeMocked _dependency;
ClassUnderTest(IDependencyToBeMocked dependency) {
_dependency = dependency;
}
public async Task MethodToBeTested(CancellationToken token) {
while (!stoppingToken.IsCancellationRequested)
{
await _dependency.Wait();
}
}
}
public interface IDependencyToBeMocked {
Task Wait();
}
I've arranged my test like so, using xUnit and NSubstitute.
public class Tests
{
private readonly IDependencyToBeMocked _dependency;
private readonly ClassUnderTest _classUnderTest;
public Tests()
{
_dependency = Substitute.For<IDependencyToBeMocked>();
_classUnderTest = new ClassUnderTest(_dependency);
}
[Fact]
public async Task Test()
{
// arrange
var source = new CancellationTokenSource();
_dependency.Wait().Returns(Task.CompletedTask);
// act
var task = _classUnderTest.MethodToBeTested(source.Token);
await source.CancelAsync();
await task;
// assert
// do some assertions
}
}
The issue comes when running the test. It never progresses past the line var task = _classUnderTest.MethodToBeTested(source.Token);
and the cancellation token never gets cancelled.
The method to be tested never returns back to the caller, despite the fact it is not awaited. I'd expect the first time it hits await _dependency.Wait()
for control to return to the caller (i.e. the Test() method), but instead it remains perpetually stuck in the while loop.
There definitely seems to be some weirdness happening: If I change it so _dependency.Wait().Returns(Task.Delay(10));
the test then progresses as expected; however if I use _dependency.Wait().Returns(Task.Delay(1));
it remains stuck in the loop. I suspect there's something to do with the task scheduler deciding whether an await is going to hold up a thread or not before returning to the caller?
Not sure if I'm misunderstanding how async methods are meant to be mocked or something.
Thank you!
Upvotes: 0
Views: 95
Reputation: 570
You can use TaskCompletionSource:
[Fact]
public async Task Test()
{
var tcs = new TaskCompletionSource<object>();
var source = new CancellationTokenSource();
_dependencyMock.Wait().Returns(tcs.Task);
var task = _classUnderTest.MethodToBeTested(source.Token);
tcs.SetResult(new object()); // Finish internal async operation somehow
await source.CancelAsync();
await task;
}
Upvotes: 0
Reputation: 716
The async state maschine only registers a callback if there is asynchronous work to do and finishes synchonously if possible.
By returning Task.Completed Task
you have code that finishes synchonously and thus controlflow is not returned. Task.Delay
on the other hand schedules a callback and thus controlflow is returned.
Nice find!
Upvotes: 1