3

I am using .NET Core 2.0 and the .NET Core MongoDB driver.

I have created a repository like so:

public interface IRepository<T>
{
    IMongoQueryable<T> Get()
}

I have done this to give flexibility to whoever uses this to be able to do LINQ much like they would do using EF. The problem is when it comes to unit testing and I'm trying to create an in-memory database so I can check states before and after operation.

Some stuff I tried:

public class InMemoryRepository : IRepository<ConcreteType>
{
    private HashSet<ConcreteType> _data = new HashSet<ConcreteType>();

    public IMongoQueryable<ConcreteType> Get()
    {
        return (IMongoQueryable<ConcreteType>)_data.AsQueryable();
    }
}

The case doesn't work as the interface for IMongoQueryable is:

public interface IMongoQueryable<T> : IMongoQueryable, IQueryable, IEnumerable, IQueryable<T>, IEnumerable<T>, IAsyncCursorSource<T>

Another go:

public class InMemoryRepository : IRepository<ConcreteType>
{
    private HashSet<ConcreteType> _data = new HashSet<ConcreteType>();

    public InMemoryRepository()
    {
        _mongoQueryableMock = new Mock<IMongoQueryable<ConcreteType>>();
        _mongoQueryableMock.Setup(m => m.AsQueryable()).Returns(_data.AsQueryable);
    }

    public IMongoQueryable<ConcreteType> Get()
    {
        return _mongoQueryableMock.Object;
    }
}

This doesn't work as IMongoQueryable.AsQueryable() is an extension method and I can't mock/setup that.

Kevin Lee
  • 1,104
  • 3
  • 13
  • 33
  • 3
    Mocking the IQueryable interface is always difficult and dangerous, because you cannot correctly mimic the behaviour of the implementation. As a result, your might pass however it won't work against a real MongoDb instance. The role of repository is to abstract away the data access technology from the business logic of your application. If your repository exposes MongoDb specific interfaces to your business layer, then you have a leaky abstraction. You are risking to tie your business logic to a specific data access technology. – Botond Botos Sep 04 '17 at 11:02

2 Answers2

3

Configure the mock to be able to handle IQueryable calls.

public class InMemoryRepository : IRepository<ConcreteType> {
    private HashSet<ConcreteType> _data = new HashSet<ConcreteType>();
    private Mock<IMongoQueryable<ConcreteType>> _mongoQueryableMock;

    public ReviseMeasureRepository() {    
        var queryableList = _data.AsQueryable();

        _mongoQueryableMock = new Mock<IMongoQueryable<ConcreteType>>();
        _mongoQueryableMock.As<IQueryable<ConcreteType>>().Setup(x => x.Provider).Returns(queryableList.Provider);
        _mongoQueryableMock.As<IQueryable<ConcreteType>>().Setup(x => x.Expression).Returns(queryableList.Expression);
        _mongoQueryableMock.As<IQueryable<ConcreteType>>().Setup(x => x.ElementType).Returns(queryableList.ElementType);
        _mongoQueryableMock.As<IQueryable<ConcreteType>>().Setup(x => x.GetEnumerator()).Returns(() => queryableList.GetEnumerator());    
    }

    public IMongoQueryable<ConcreteType> Get() {
        return _mongoQueryableMock.Object;
    }

    //...
}

With that out of the way I think the design of the repository is leaky and directly couples your code to external dependencies. Consider reviewing the design of the repository abstraction.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I'm not sure if this is out of the scope of this question but do you have any ideas on that? If I wanted to use say `.Take()` and `.Offset()` I would have to write specific methods for those. I tried to make it so that the repository wasn't coupled and succeeded with the other CRUD methods but I couldn't find happy medium for returning the collection. I obviously don't want to return the everything in the database and let them filter what they want in memory. – Kevin Lee Sep 04 '17 at 13:02
  • 1
    @KevinLee, That is a whole new question. That is also the problem with abstracting for abstraction sake. Abstract the desired behaviors. So make your API more targeted to what you want to expose. Exposing IQueryable is leaky. `.Take()` and `.Offset()` are implementation concerns that should be encapsulated by your implementation of the desired behavior. – Nkosi Sep 04 '17 at 13:16
0

I was struggling with a similar task and landed on this question. I was not able to mock IMongoQueryable interface. But, what I came to realize is you don't really have to mock it. IMongoQueryable<T> implements IQueryable<T>. As long as you want to test filtering logic, you can use IQueryable<T> interface instead. In my case, I created a QueryBuilder class that accepts IQueryable<T> as a c-tor argument. Then, It exposes several methods that build the actual filter

{
    public class MyCollectionQueryBuilder
    {
        private IQueryable<MyCollectionItem> query;
        public MyCollectionQueryBuilder(IQueryable<MyCollectionItem> query)
        {
            this.query = query;
        }
        public MyCollectionQueryBuilder WithCol1Filter(string filter)
        {
            query = query.Where(a => (a.Col1 == filter));

            return this;
        }

        public IQueryable<CoreFolderResearchModelLink> Build()
        {
            return query;
        }
    }
}

Then, in the real code it'll be called like that

{
    var queryBuilder = new MyCollectionQueryBuilder(myMongoCollection.AsQueryable()); //IMongoQueryable
    q = queryBuilder.WithCol1Filter("filter_value").Build();
    var res = q.ToList();
}

And Unit Tests will call it like below

{
    private IEnumerable<MyCollectionItem> inputData = new List<MyCollectionItem> {};
    var queryBuilder = new MyCollectionQueryBuilder(inputData.AsQueryable()); //Linq IQueryable
    q = queryBuilder.WithCol1Filter("filter_value").Build();
    var res = q.ToList();
}

At runtime both are resolved to correct interface. So, you can assert the results without connecting to the database

DanielS
  • 744
  • 6
  • 13
  • The problem is that you might want to call q.ToListAsync(), in which case IQueryable won't cut it. You need IMongoQueryable. – CarlJ May 06 '22 at 02:12