Reputation:
I am writing an ASP.NET console application to practice mocking Entity Framework with MOQ for testing. The app manages a book store and has a basic EditPrice
method as shown below:
public class BookStore
{
private BookContext context;
public BookStore(BookContext newContext)
{
context = newContext;
}
// Edit the price of a book in the store
public Book EditPrice(int id, double newPrice)
{
Book book = context.Books.Single(b => b.Id == id);
book.Price = newPrice;
context.SaveChanges();
return book;
}
}
This method is tested with the following test method:
[TestMethod]
public void Test_EditPrice()
{
// Arrange
var mockSet = new Mock<DbSet<Book>>();
var mockContext = new Mock<BookContext>();
mockContext.Setup(m => m.Books).Returns(mockSet.Object);
var service = new BookStore(mockContext.Object);
service.AddBook(1, "Wuthering Heights", "Emily Brontë", "Classics", 7.99, 5);
// Act
service.EditPrice(1, 5.99);
// Assert
mockSet.Verify(m => m.Add(It.IsAny<Book>()), Times.Once());
mockContext.Verify(m => m.SaveChanges(), Times.Exactly(2));
}
This method fails throwing the following error:
Message: Test method BookStoreNonCore.Tests.NonQueryTests.Test_EditPrice threw exception:
System.NotImplementedException: The member 'IQueryable.Provider' has not been implemented on type 'DbSet'1Proxy' which inherits from 'DbSet`1'. Test doubles for 'DbSet'1' must provide implementations of methods and properties that are used.
Following along with the debugger, the test fails on the line in the main EditPrice
method
Book book = context.Books.Single(b => b.Id == id);
I haven't quite got to grips with mock testing yet and am not sure why this is failing. Would anyone be able to explain and provide a solution?
Upvotes: 3
Views: 5159
Reputation: 169
I recall that when using a Mock I ran into problems when testing asynchronous EF operations.
To fix that you can distill an interface from your DbContext and create a second "Fake" DbContext. This Fake could contain a number of FakeDbSet classes (inheriting DbSet).
Check out this MS documentation, more specifically the part "Testing with async queries": https://learn.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace TestingDemo
{
internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
private readonly IQueryProvider _inner;
internal TestDbAsyncQueryProvider(IQueryProvider inner)
{
_inner = inner;
}
public IQueryable CreateQuery(Expression expression)
{
return new TestDbAsyncEnumerable<TEntity>(expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new TestDbAsyncEnumerable<TElement>(expression);
}
public object Execute(Expression expression)
{
return _inner.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return _inner.Execute<TResult>(expression);
}
public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(expression));
}
public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute<TResult>(expression));
}
}
internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
: base(enumerable)
{ }
public TestDbAsyncEnumerable(Expression expression)
: base(expression)
{ }
public IDbAsyncEnumerator<T> GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<T>(this); }
}
}
internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
private readonly IEnumerator<T> _inner;
public TestDbAsyncEnumerator(IEnumerator<T> inner)
{
_inner = inner;
}
public void Dispose()
{
_inner.Dispose();
}
public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_inner.MoveNext());
}
public T Current
{
get { return _inner.Current; }
}
object IDbAsyncEnumerator.Current
{
get { return Current; }
}
}
}
The FakeDbSet class needs to have a few overrides to return these different implementations, also mentioned in the documentation:
var mockSet = new Mock<DbSet<Blog>>();
mockSet.As<IDbAsyncEnumerable<Blog>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));
mockSet.As<IQueryable<Blog>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider));
mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
Except instead of setting this up in a Mock, it's just a method override in your own class.
The advantage of this is that you can set up fake data in your unit tests in a more compact and readable way than setting up mocks and fake returns. For example:
[TestClass]
public class BookTest
{
private FakeBooksDbContext context;
[TestInitialize]
public void Init()
{
context = new FakeBooksDbContext();
}
[TestMethod]
public void When_PriceIs10_Then_X()
{
// Arrange
SetupFakeData(10);
// Act
// Assert
}
private void SetupFakeData(int price)
{
context.Books.Add(new Book { Price = price });
}
}
In EFCore, all of this is irrelevant and you can just use an in-memory database type of course.
Upvotes: 1
Reputation:
I solved it by using a Linq query rather than the Single member:
// Edit the price of a book in the store
public void EditPrice(int id, double newPrice)
{
var query = from book in context.Books
where book.Id == id
select book;
Book BookToEdit = query.ToList()[0];
BookToEdit.Price = newPrice;
context.SaveChanges();
}
Then followed the example on this website for Testing Query Scenrios
https://learn.microsoft.com/en-gb/ef/ef6/fundamentals/testing/mocking
to write this test method which now works:
[TestMethod]
public void Test_EditPrice()
{
// Arrange
var data = new List<Book>
{
new Book(1, "Wuthering Heights", "Emily Brontë", "Classics", 7.99, 5)
}.AsQueryable();
var mockSet = new Mock<DbSet<Book>>();
mockSet.As<IQueryable<Book>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Book>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Book>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Book>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var mockContext = new Mock<BookContext>();
mockContext.Setup(c => c.Books).Returns(mockSet.Object);
// Act
var service = new BookStore(mockContext.Object);
var books = service.GetAllBooks();
service.EditPrice(1, 5.99);
// Assert
Assert.AreEqual(data.Count(), books.Count);
Assert.AreEqual("Wuthering Heights", books[0].Title);
Assert.AreEqual(5.99, books[0].Price);
}
Thank you to both of you for pointing me in the right direction (or at least away from the cause of the problem).
Upvotes: 2
Reputation: 1497
From what I remember mocking entity framework in this way is VERY difficult, I suggest that if you are very adamant about testing the framework in this way then it may be better to wrap your context in an interface IBookContext
and have your own methods wrapping the functionality of entity framework so that things are more easily moqable and you don't have to deal with entity framework.
If you are using .Net core then there is an InMemory provider that you can use: https://learn.microsoft.com/en-us/ef/core/providers/in-memory/
If you are using the framework then there is a testing framework called Effort: https://entityframework-effort.net/
Both are in memory implementations of entity framework - you can use these in tests so you don't have to integrate with a database (which is slow)
Upvotes: 2