简体   繁体   中英

Failing test while trying to mock Entity Framework with MOQ

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?

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.

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)

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://docs.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).

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://docs.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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM