Search code examples
c#mongodbunit-testingnsubstitute

NSubstitute mocking Mongo IFindFluent does not work


I was doing some unit tests around my repository, until I got a strange error. Searched around to see if I was not making known mistakes, I could simplify it and notice that I am getting the same error. Looks like I cannot mock IFindFluent interface properly, and I would like to know what I am doing wrong. This test:

    [Fact]
    public void Test()
    {
        var ff = Substitute.For<IFindFluent<string, string>>();
        ff.FirstOrDefaultAsync().Returns("asd");
    }

is returning this error message:

NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException : Can not return value of type Task`1 for IDisposable.Dispose (expected type Void).

Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)), and that you are not configuring other substitutes within Returns() (for example, avoid this: mySub.SomeMethod().Returns(ConfigOtherSub())).

If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member. Return values cannot be configured for non-virtual/non-abstract members.

Correct use: mySub.SomeMethod().Returns(returnValue);

Potentially problematic use: mySub.SomeMethod().Returns(ConfigOtherSub()); Instead try: var returnValue = ConfigOtherSub(); mySub.SomeMethod().Returns(returnValue);

at NSubstitute.Core.ConfigureCall.CheckResultIsCompatibleWithCall(IReturn valueToReturn, ICallSpecification spec) at NSubstitute.Core.ConfigureCall.SetResultForLastCall(IReturn valueToReturn, MatchArgs matchArgs) at NSubstitute.Core.SubstitutionContext.LastCallShouldReturn(IReturn value, MatchArgs matchArgs) at CorporateActions.Tests.Persistence.RepositoryTests.Test()

I've searched around, but most common mistakes about it does not fit with this simple implementation. Any thoughts why does this not work?


Solution

  • FirstOrDefaultAsync is an extension method for IFindFluent<TDocument, TProjection>. Unfortunately, NSubstitute does not support mocking of extension methods.

    However it does not mean you can't develop proper UT in this case. Extension methods for interfaces eventually call some method(s) of those interfaces. So the common workaround for this problem is to check what interface methods are actually called and mock them, instead of mocking whole extension method.

    IFindFluentExtensions.FirstOrDefaultAsync() is defined as:

    public static Task<TProjection> FirstOrDefaultAsync<TDocument, TProjection>(this IFindFluent<TDocument, TProjection> find, CancellationToken cancellationToken = default(CancellationToken))
    {
        Ensure.IsNotNull(find, nameof(find));
    
        return IAsyncCursorSourceExtensions.FirstOrDefaultAsync(find.Limit(1), cancellationToken);
    }
    

    Now you see that you should mock IFindFluent<TDocument, TProjection>.Limit(int? limit) method and dig into IAsyncCursorSourceExtensions.FirstOrDefaultAsync() extension method.

    IAsyncCursorSourceExtensions.FirstOrDefaultAsync is defined as:

    public static async Task<TDocument> FirstOrDefaultAsync<TDocument>(this IAsyncCursorSource<TDocument> source, CancellationToken cancellationToken = default(CancellationToken))
    {
        using (var cursor = await source.ToCursorAsync(cancellationToken).ConfigureAwait(false))
        {
            return await cursor.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
        }
    }
    

    So you need to mock IAsyncCursorSource<TDocument>.ToCursorAsync() and check IAsyncCursorExtensions.FirstOrDefaultAsync() extension method.

    IAsyncCursorExtensions.FirstOrDefaultAsync() is defined as:

    public static async Task<TDocument> FirstOrDefaultAsync<TDocument>(this IAsyncCursor<TDocument> cursor, CancellationToken cancellationToken = default(CancellationToken))
    {
        using (cursor)
        {
            var batch = await GetFirstBatchAsync(cursor, cancellationToken).ConfigureAwait(false);
            return batch.FirstOrDefault();
        }
    }
    

    So the last method to analyze is IAsyncCursorExtensions.GetFirstBatchAsync(), which is defined as:

    private static async Task<IEnumerable<TDocument>> GetFirstBatchAsync<TDocument>(IAsyncCursor<TDocument> cursor, CancellationToken cancellationToken)
    {
        if (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
        {
            return cursor.Current;
        }
        else
        {
            return Enumerable.Empty<TDocument>();
        }
    }
    

    Here we see that we should also mock IAsyncCursor.MoveNextAsync() and IAsyncCursor.Current.

    Here is the test method that mocks all discovered calls:

    [Fact]
    public void TestMethod()
    {
        var cursorMock = Substitute.For<IAsyncCursor<string>>();
        cursorMock.MoveNextAsync().Returns(Task.FromResult(true), Task.FromResult(false));
        cursorMock.Current.Returns(new[] { "asd" });
    
        var ff = Substitute.For<IFindFluent<string, string>>();
        ff.ToCursorAsync().Returns(Task.FromResult(cursorMock));
        ff.Limit(1).Returns(ff);
    
        var result = ff.FirstOrDefaultAsync().Result;
        Assert.AreEqual("asd", result);
    }