not_only_but_also
not_only_but_also

Reputation: 123

Mocked dependency with an async method never returns control to caller

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

Answers (2)

Ilia
Ilia

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

Clemens
Clemens

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

Related Questions