Motivation
The objective of this post is to test an asynchronous
service operation, and in particular methods which include Entity Framework 6.x asynchronous queries. When conducting unit testing of asynchronous code my
starting point had to be a post by Stephen Cleary. This article introduces the reader to the subject. providing general guidelines, what to avoid, valuable insights, etc.
His blog is also a valuable resource when undertaking asynchronous programming.
Code to be Tested
The following methods in based on the Northwind database and it computes the running totals for each customer of the store including the number of orders and how much they have spent historically. The method is an operation of a service called CustomerOrderAggregation and it takes a search criteria containing CustomerID. The idea is if the search criteria CustomerId value is set then the computation is executed for that customer and if not set then the computation is done for all customers.
public async Task<IQueryable<CustomerOrderStatistic>> CustomerOrderSummaryAsync(
CustomerOrderSearch criteria)
{
var result = await customerRepository
.GetAll()
.AsNoTracking()
.WhereIf(
!string.IsNullOrEmpty(criteria.CustomerId),
r => r.CustomerID.Equals(criteria.CustomerId))
.GroupJoin(
OrderOrderDetailsStatistics(criteria),
c => c.CustomerID,
o => o.CustomerId,
(c, o) => new { c, o })
.SelectMany(
x => x.o.DefaultIfEmpty(),
(x, l) => new CustomerOrderStatistic
{
CustomerId = x.c.CustomerID,
ContactName = x.c.ContactName,
TotalPayments = l != null ? l.TotalPayments : 0,
TotalOrders = l != null ? l.TotalOrders : 0,
}).ToListAsync();
return result.AsQueryable();
}
OrderOrderDetailsStatistics(criteria) executes a join between Order and Order_Details on OrderId and groups by Order.CustomerId and this is how we learn the running totals:
private IQueryable<CustomerOrderGrouped> OrderOrderDetailsStatistics(
CustomerOrderSearch criteria)
{
return orderRepository.GetAll()
.AsNoTracking()
.Join(
this.orderDetailsRepository.GetAll().AsNoTracking(),
o => o.OrderID,
od => od.OrderID,
(o, od) => new { o, od })
.GroupBy(g => new { g.o.CustomerID })
.Select(grp => new CustomerOrderGrouped
{
CustomerId = grp.Key.CustomerID,
TotalPayments = grp.Sum(row => (row.o.Freight.HasValue ? row.o.Freight.Value : 0) +
(row.od.UnitPrice * row.od.Quantity)),
TotalOrders = grp.Select(row => row.o.OrderID)
.Distinct()
.Count()
})
.WhereIf(
!string.IsNullOrEmpty(criteria.CustomerId),
r => r.CustomerId.Equals(criteria.CustomerId));
}
Unit Testing with Moq
The service construction is as follows:
public CustomerOrderAggregation(
ICustomerRepository customerRepository,
IOrderDetailsRepository orderDetailsRepository,
IOrderRepository orderRepository)
{
this.customerRepository = customerRepository;
this.orderDetailsRepository = orderDetailsRepository;
this.orderRepository = orderRepository;
}
hinting that for the unit tests using Moq three repositories need to be mocked. Repositories GetAll() is a generic which returns an IQueryable
therefore to be able to execute the queries against the mocked IQueryable GetAll() IQueryable needs to be implemented. This is explained in the reference provided in this post: var data = new List<Customer>
{
new Customer
{
CustomerID = "LAUGB",
CompanyName = "Laughing Bacchus Wine Cellars",
ContactName = "Yoshi Tannamuri",
ContactTitle = "Marketing Assistant"
}.AsQueryable();
};
var mockSet = new Mock<DbSet<Customer>>();
mockSet.As<IQueryable<Customer>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Customer>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Customer>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Customer>>().Setup(m => m.GetEnumerator()).Returns(() => data.GetEnumerator());
customerRepositoryMock.Setup(c => c.GetAll()).Returns(mockSet.Object);
OrderRepository and OrderDetailsRepository GetAll() would need to me mocked using the same procedure for Order and Order_Details respectively. However, trying to use the above mocked repositories will fail for two reasons.
First AsNoTracking() needs to be mocked as well:
mockSet.As<IQueryable<Customer>>()Setup(c => c.AsNoTracking()).Returns(dbSet.Object);
However, the test for the async method would fail with the following exception:
"
System.InvalidOperationException: The source IQueryable doesn't implement IDbAsyncEnumerable. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068."The solution to the error is in the link provided in the exception message!!! The reference is telling us that to be able to process the async method a DbAsyncQueryProvider needs to be created. I selected to follow the same solution
discussed in the msdn post because it is reusable. The only difference is that the mocked DbSet was encapsulated in a static class with a static generic method to facilitate reusability.
public static class MockDbSet
{
public static DbSet<T> GetQueryableMockDbSet<T>(params T[] sourceList) where T : class
{
var queryable = sourceList.AsQueryable();
var dbSet = new Mock<DbSet<T>>();
dbSet.As<IDbAsyncEnumerable<T>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<T>(queryable.GetEnumerator()));
dbSet.As<IQueryable<T>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<T>(queryable.Provider));
dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
dbSet.Setup(c => c.AsNoTracking()).Returns(dbSet.Object);
return dbSet.Object;
}
}
Now repositories are mocked this way in the test setup method:
[TestInitialize]
public void Setup()
{
// Arrange - Mock Repositories
var customerRepositoryMock = new Mock<ICustomerRepository>();
var orderDetailsRepositoryMock = new Mock<IOrderDetailsRepository>();
var orderRepositoryMock = new Mock<IOrderRepository>();
// Arrange - Setup return values
customerRepositoryMock.Setup(c => c.GetAll()).Returns(MockDbSet.GetQueryableMockDbSet<Customer>(
new Customer
{
CustomerID = "LAUGB",
CompanyName = "Laughing Bacchus Wine Cellars",
ContactName = "Yoshi Tannamuri",
ContactTitle = "Marketing Assistant"
},
new Customer
{
CustomerID = "JMARINO",
CompanyName = "Marino's",
ContactName = "Jose Marino",
ContactTitle = "Principal"
}));
// Arrange - setup order details
orderDetailsRepositoryMock.Setup(o => o.GetAll()).Returns(MockDbSet.GetQueryableMockDbSet<Order_Detail>(
new Order_Detail { OrderID = 1, ProductID = 23, UnitPrice = 7.20m, Quantity = 10 },
new Order_Detail { OrderID = 1, ProductID = 41, UnitPrice = 7.70m, Quantity = 20 },
new Order_Detail { OrderID = 1, ProductID = 77, UnitPrice = 10.40m, Quantity = 5 },
new Order_Detail { OrderID = 2, ProductID = 24, UnitPrice = 4.50m, Quantity = 5 },
new Order_Detail { OrderID = 2, ProductID = 52, UnitPrice = 7.00m, Quantity = 5 },
new Order_Detail { OrderID = 3, ProductID = 13, UnitPrice = 6.00m, Quantity = 7 },
new Order_Detail { OrderID = 3, ProductID = 25, UnitPrice = 14.00m, Quantity = 5 },
new Order_Detail { OrderID = 3, ProductID = 70, UnitPrice = 15.00m, Quantity = 5 }));
// Arrange - setup orders
orderRepositoryMock.Setup(
o => o.GetAll())
.Returns(
MockDbSet.GetQueryableMockDbSet<Order>(
new Order { OrderID = 1, CustomerID = "LAUGB", Freight = 4.65m },
new Order { OrderID = 2, CustomerID = "LAUGB", Freight = 0.94m },
new Order { OrderID = 3, CustomerID = "LAUGB", Freight = 4.33m }));
// Arrange - create instance of service and inject mocked repositories
_customerOrderAggregationService = new CustomerOrderAggregation(
customerRepositoryMock.Object,
orderDetailsRepositoryMock.Object, orderRepositoryMock.Object);
}
and the service is tested like this:
[TestMethod]
public async Task Test_CustomerOrderSummaryAsync_ForExcistingUser_Returns_Summary_With_One_Group()
{
// Arrange - Create search criteria
var criteria = new CustomerOrderSearch
{
CustomerId = "LAUGB"
};
// Act - Call Service async method
var result = await _customerOrderAggregationService
.CustomerOrderSummaryAsync(criteria);
// Assert - validate only one group is present
var resultList = result.ToList();
Assert.IsTrue(resultList.Count == 1);
}
Conclusion
- Mocking IQueryable using Moq for a repository of T the mocked DbSet IQueriable needs to setup: query Provider, query Expression, query ElementType, GetEnumarator()
- When testing async queries the mocked DbSet needs to set up e GetEnumerator() for an IDbAsyncEnumerator and setup and AsyncQueryProvider
References