Monday, July 10, 2017

Testing EF 6.x Async Queries With Moq: In Memory DbAsyncQueryProvider

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

  1. Mocking IQueryable using Moq for a repository of T the mocked DbSet IQueriable needs to setup: query Provider, query Expression, query ElementType, GetEnumarator()
  2. When testing async queries the mocked DbSet needs to set up e GetEnumerator() for an IDbAsyncEnumerator and setup and AsyncQueryProvider

References

  1. Async Programming: Unit Testing of Asynchronous Code: https://msdn.microsoft.com/en-us/magazine/dn818493.aspx 
  2. Explanation to InvalidOperationException when testing EF 6.x async queries: https://msdn.microsoft.com/en-gb/data/dn313107
  3.  Detailed explanation and reference code to be able to use moq mock objects in blocking and non blocking scenarios:  https://msdn.microsoft.com/en-gb/data/dn314429

13 comments:

  1. This is really the sort of data I have been attempting to discover. Much obliged to you for composing this data. quality assurance software testing 

    ReplyDelete
  2. Thank you for your comment mind.it!

    ReplyDelete
  3. The information innovations have encouraged the advancement of improved mail arrange retailing, in which merchandise can be requested rapidly by utilizing phones or PC systems and after that dispatched by providers through coordinated transport organizations that depend widely on PCs and correspondence advances to control their activities. Freelance Automation QA Engineer

    ReplyDelete
  4. I was recommended this web site by means of my cousin.
    I am now not certain whether this post is written through him as nobody else recognise such precise about my difficulty. You're amazing! Thank you!

    selenium training in Chennai
    selenium training in Tambaram
    selenium training in Velachery
    selenium training in Omr
    selenium training in Annanagar

    ReplyDelete
  5. At the point when the coding is done, the software engineer sends their work to the Software Quality Assurance division/faculty. They will disregard crafted by the software engineers.Open Source crowdfunding software for sale

    ReplyDelete
  6. Are https://adobe.cheapsoftwaredownload.net/ thinking about starting a business in 2019, but don't know how or even where to begin? This article outlines the biggest obstacles to overcome, what you need to start your business, and what to do after year one!

    ReplyDelete
  7. Quick one page guide explaining what payday loans are for, who can get them, how to qualify and the charges associated with them. In summary, anyone who is employed, over 18 and has a bank account has a good chance of being accepted for a payday loan online today. buymodafinilonline.reviews

    ReplyDelete
  8. So you started your own business. You've seen a hole in the market or come up with a brilliant new idea. You've got things underway and maybe even started to make a bit of money. what is 3d coat

    ReplyDelete
  9. Wow that was unusual. I just wrote an really long comment but after I clicked submit my comment didn’t appear. Grrrr… well I’m not writing all that over again. Regardless, just wanted to say great blog! Miss feather hair pieces putlocker

    ReplyDelete
  10. Gangaur Realtech is a professionally managed organisation specializing in real estate services where integrated services are provided by professionals to its clients seeking increased value by owning, occupying or investing in real estate. wine shop europe online

    ReplyDelete
  11. The two definitions rotate around something very similar - application and utilization.
    best microphone for streaming

    ReplyDelete
  12. SOFTWARE Maintenance exercises incorporate all work did post-conveyance and ought to be recognized from square alterations which speak to critical structure and improvement exertion and supplant a formerly discharged software bundle.download itools 4 full crack

    ReplyDelete
  13. To answer tyour question on what is what is test conditions. I have the perfect platform to read form. Follow this link for more information

    ReplyDelete

A1 Repo: Simple Pagination for WCF Service Operation

It is never a good idea to paginate on the client.  This post is about a simple pagination for a WCF service operation.  For this work I nee...