Mock returning a List as IMongoQueryable for unit testing
2020-01-28 17:29
The Problem
The latest MongoDb driver for .Net doesn't have a way to convert a collection such as List
The Solution
What you'll need to run this sample yourself.
- Visual Studio
- MongoDb.Driver
- NSubstitute
- xunit
- xunit.runner.visualstudio
Let's say you've settled on using MongoDb as your NoSQL data store. You write a simple repository pattern with one method to query for any concrete type.1
public interface IMongoRepository<T> where T : class
{
public IMongoQueryable<T> QueryAll();
}
You also have a simple Customer service that calls the repository
public class CustomerService
{
IMongoRepository<Customer> _customerRepository = null;
public CustomerService(IMongoRepository<Customer> customerRepository)
{
_customerRepository = customerRepository;
}
public List<Customer> GetCustomers()
{
return _customerRepository.QueryAll().ToList();
}
}
Finally, you start writing the following test. But you discover there's no way to get a concrete instance of IMongQueryable
public class CustomerService_Should
{
[Fact]
public void Return_customers()
{
var expected = new List<Customer>() { new Customer() { Id = 1 } };
var customerRepository = Substitute.For<IMongoRepository<Customer>>();
//return the mocked data. But how to convert the list into IMongoQueryable???
customerRepository.QueryAll().Returns([argh, what goes here??]);
var service = new CustomerService(customerRepository);
var actual = service.GetCustomers();
Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected.First().Id, actual.First().Id);
}
}
Like me, you probably try all kinds of typecasting before realizing you're always trying to do something impossible. Finally, you find the answer on Stack Overflow. There are two ways to mock up the data, and both make the IMongoQueryable class accept IQueryable.
Using NSubstitute
public class CustomerService_Should
{
[Fact]
public void Return_customers()
{
var expected = new List<Customer>() { new Customer() { Id = 1 } };
var customerRepository = Substitute.For<IMongoRepository<Customer>>();
//Mock IMongoQueryable to accept IQueryable, enabling just enough of the interface
//to work
var expectedQueryable = expected.AsQueryable();
var mockQueryable = Substitute.For<IMongoQueryable<Customer>>();
mockQueryable.ElementType.Returns(expectedQueryable.ElementType);
mockQueryable.Expression.Returns(expectedQueryable.Expression);
mockQueryable.Provider.Returns(expectedQueryable.Provider);
mockQueryable.GetEnumerator().Returns(expectedQueryable.GetEnumerator());
//return the mocked data
customerRepository.QueryAll().Returns(mockQueryable);
var service = new CustomerService(customerRepository);
var actual = service.GetCustomers();
Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected.First().Id, actual.First().Id);
}
}
Pretty slick.
Creating MongoQueryable
A simple concrete class that allows setting a List property.
public class MongoQueryable<T> : IMongoQueryable<T>
{
public List<T> MockData { get; set; }
public Type ElementType => MockData.AsQueryable().ElementType;
public Expression Expression => MockData.AsQueryable().Expression;
public IQueryProvider Provider => MockData.AsQueryable().Provider;
public IEnumerator<T> GetEnumerator() => MockData.AsQueryable().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => MockData.AsQueryable().GetEnumerator();
public QueryableExecutionModel GetExecutionModel() => throw new NotImplementedException();
public IAsyncCursor<T> ToCursor(CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<IAsyncCursor<T>> ToCursorAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
}
The test.
[Fact]
public void Return_customers2()
{
var expected = new List<Customer>() { new Customer() { Id = 1 } };
var customerRepository = Substitute.For<IMongoRepository<Customer>>();
//Mock IMongoQueryable using a class
var mockQueryable = new MongoQueryable<Customer>();
mockQueryable.MockData = expected;
//return the mocked data
customerRepository.QueryAll().Returns(mockQueryable);
var service = new CustomerService(customerRepository);
var actual = service.GetCustomers();
Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected.First().Id, actual.First().Id);
}
References
-
The comments in Stack Overflow point out that using IMongoQueryable--or IQueryable-- isn't ideal because it tightly couples the code to MongoDb or to a Queryable backend. It might be better to use a truly generic repository and convert to/from MongoDb (or other database) as needed.↩