Mock setup for Marten.IDocumentSession (Moq/Nunit)

324 Views Asked by At

I am trying to mock this statement:

IReadOnlyList<Student> students = await _session
    .Query<Student>()
    .Where(x => x.ClassId == classId)
    .ToListAsync(cancellationToken);

My attempt at is:

private Mock<IDocumentSession> _sessionMock = new Mock<IDocumentSession>();
...
_sessionMock
    .Setup(x => x
        .Query<Students>()
        .Where(y => y.ClassId == classId)
        .ToListAsync(CancellationToken.None))
    .ReturnsAsync(new List<Students));       

But i am getting this error:

System.NotSupportedException : Unsupported expression: ... => ....ToListAsync(CancellationToken.None) Extension methods (here: QueryableExtensions.ToListAsync) may not be used in setup / verification expressions.

I looked it up and read the answers I am getting from SOF and other places and understood that basically it's not easily possible to test extension methods.

The answers are old, like 5+ years, some from 2011, since then is there a way to get this to work?

2

There are 2 best solutions below

5
Peter Csala On

TL;DR: I did not find any working solution to be able to mock IMartenQueryable


The IDocumentSession interface has the following inheritance chain:

IDocumentSession << IDocumentOperations << IQuerySession

Based on the source code the Query method is defined on IQuerySession interface like this

/// <summary>
///     Use Linq operators to query the documents
///     stored in Postgresql
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
IMartenQueryable<T> Query<T>();

The IMartenQueryable<T> is indeed an IQueryable<T>.

And that could be easily mocked via the MockQueryable.Moq.

List<Student> students = ...
var queryableMock = students.BuildMock();
_sessionMock.Setup(x => x.Query<Student>()).Returns(queryableMock);

I haven't tested the code, maybe you have to cast it to IMartenQueryable.


UPDATE #1

Based on this QueryableExtensions we should be able to convert IQueryable<Student> to IMartenQueryable<Student> via the As operator.

The As defined inside the JasperFx.Core.Reflection namespace.

I've created to convert

var queryableMock = students.AsQueryable().As<IMartenQueryable<Student>>();
//OR
var queryableMock = students.BuildMock().As<IMartenQueryable<Student>>();

but unfortunately it fails with InvalidCastException.

Tomorrow I'll continue my investigation from here.


UPDATE #2

As it turned out the As<T> function is just a simple wrapper around type cast... So, it did not help anything.

I've also tried to mock directly the IMartenQueryable<T> and it's ToListAsync member method. The problem with this approach is that you need to rewrite your production query to filter elements in memory << kills to whole point of having an IQueryable<T> (or a derived interface).

So, I gave up, I don't have any idea, how to do it properly. But as I have seen in the documentation and in the issues we are not the only ones :D

1
broniu On

I had similar problem and I deatl with it by creating class MartenQueryableStub:

internal class MartenQueryableStub<T> : List<T>, IMartenQueryable<T>
{
    private readonly Mock<IQueryProvider> queryProviderMock = new();
    public Type ElementType => typeof(T);

    public Expression Expression => Expression.Constant(this);

    public IQueryProvider Provider
    {
        get
        {
            queryProviderMock
                .Setup(x => x.CreateQuery<T>(It.IsAny<Expression>()))
                .Returns(this);
            return queryProviderMock.Object;
        }
    }

    public QueryStatistics Statistics => throw new NotImplementedException();

    public Task<bool> AnyAsync(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<double> AverageAsync(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<int> CountAsync(CancellationToken token) 
    {
        throw new NotImplementedException();
    }

    public Task<long> CountLongAsync(CancellationToken token) 
    {
        throw new NotImplementedException();
    }

    public QueryPlan Explain(FetchType fetchType = FetchType.FetchMany, Action<IConfigureExplainExpressions>? configureExplain = null)
    {
        throw new NotImplementedException();
    }

    public Task<TResult> FirstAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<TResult?> FirstOrDefaultAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public IMartenQueryable<T> Include<TInclude>(Expression<Func<T, object>> idSource, Action<TInclude> callback) where TInclude : notnull
    {
        throw new NotImplementedException();
    }

    public IMartenQueryable<T> Include<TInclude>(Expression<Func<T, object>> idSource, IList<TInclude> list) where TInclude : notnull
    {
        throw new NotImplementedException();
    }

    public IMartenQueryable<T> Include<TInclude, TKey>(Expression<Func<T, object>> idSource, IDictionary<TKey, TInclude> dictionary)
        where TInclude : notnull
        where TKey : notnull
    {
        throw new NotImplementedException();
    }

    public Task<TResult> MaxAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<TResult> MinAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<TResult> SingleAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public Task<TResult?> SingleOrDefaultAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public IMartenQueryable<T> Stats(out QueryStatistics stats)
    {
        throw new NotImplementedException();
    }

    public Task<TResult> SumAsync<TResult>(CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public IAsyncEnumerable<T> ToAsyncEnumerable(CancellationToken token = default)
    {
        throw new NotImplementedException();
    }

    public Task<IReadOnlyList<TResult>> ToListAsync<TResult>(CancellationToken token) =>
        Task.FromResult(this.ToList().AsReadOnly().As<IReadOnlyList<TResult>>());

And then I set up method (in my case QueryRawEventDataOnly<T>())

var myEntites = new MartenQueryableStub<MyType>() {
    myFirstEntity,
    mySecondEntity,
    ...
    myLastEntity

};
eventStoreMock.Setup(v => v.QueryRawEventDataOnly<MyType>())
    .Returns(myEntites);

In your case it shoud be something like that

var students = new MartenQueryableStub<Student>() {
    student1,
    student2,
    student3,
    ...
};
_sessionMock.Setup(v => v.Query<Student>())
    .Returns(students);