尝试使用MOQ模拟实体框架时测试失败

时间:2019-02-13 15:46:21

标签: c# asp.net entity-framework moq

我正在编写一个ASP.NET控制台应用程序,以练习用MOQ进行实体框架模拟以进行测试。该应用程序管理着一家书店,并具有基本的EditPrice方法,如下所示:

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;
    }
}

此方法通过以下测试方法进行测试:

    [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));
    }

此方法无法引发以下错误:

  

消息:测试方法BookStoreNonCore.Tests.NonQueryTests.Test_EditPrice抛出异常:
  System.NotImplementedException:成员'IQueryable.Provider'尚未在继承自'DbSet`1'的类型'DbSet'1Proxy'上实现。 'DbSet'1'的测试双打必须提供所使用的方法和属性的实现。

与调试器一起,测试在主EditPrice方法的行上失败

Book book = context.Books.Single(b => b.Id == id);

我还没有完全掌握模拟测试,并且不确定为什么会失败。谁能解释并提供解决方案?

3 个答案:

答案 0 :(得分:3)

从我记得以这种方式模拟实体框架非常困难,我建议,如果您非常坚持以这种方式测试框架,那么最好将上下文包装在接口IBookContext中,然后您可以使用自己的方法来包装实体框架的功能,从而使事情更容易实现,并且您不必处理实体框架。

两者都在实体框架的内存实现中-您可以在测试中使用它们,因此不必与数据库集成(速度很慢)

答案 1 :(得分:0)

我通过使用Linq查询而不是Single成员来解决它:

    // 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();
    }

然后按照此网站上的示例测试查询场景

  

https://docs.microsoft.com/en-gb/ef/ef6/fundamentals/testing/mocking

编写现在可以使用的测试方法:

    [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);
    }

感谢你们俩都向我指出了正确的方向(或者至少远离了问题的根源)。

答案 2 :(得分:0)

我记得在使用模拟时,在测试异步EF操作时遇到了问题。

要修复,您可以从DbContext中提取接口并创建第二个“假” DbContext。此Fake可以包含许多FakeDbSet类(继承DbSet)。

查看此MS文档,更具体地讲“使用异步查询进行测试”部分: 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; }
        }
    }
}

FakeDbSet类需要有一些覆盖,以返回这些不同的实现,在文档中也有提及:

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());

除了不是在Mock中进行设置之外,它只是您自己的类中的方法替代。

这样做的好处是,与设置模拟和假返回相比,您可以以更紧凑,更易读的方式在单元测试中设置伪数据。例如:

[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 });
    }
}

在EFCore中,所有这些都不相关,您当然可以只使用内存数据库类型。