尝试使用最小起订量模拟实体框架时测试失败



我正在编写一个 ASP.NET 控制台应用程序来练习使用最小起订量模拟实体框架进行测试。该应用程序管理书店,并具有如下所示的基本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.NotImplementException:成员 'IQueryable.Provider' 尚未在继承自 'DbSet'1' 的类型 'DbSet'1Proxy' 上实现。"DbSet'1"的测试替身必须提供所用方法和属性的实现。

跟随调试器,测试在主EditPrice方法中的行上失败

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

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

据我所知,以这种方式嘲笑实体框架非常困难,我建议,如果您非常坚持以这种方式测试框架,那么最好将您的上下文包装在接口IBookContext中,并拥有自己的方法来包装实体框架的功能,以便事情更容易移动,您不必处理实体框架。

  • 如果您使用的是 .Net 核心,则可以使用一个 InMemory 提供程序:https://learn.microsoft.com/en-us/ef/core/providers/in-memory/

  • 如果您正在使用该框架,那么有一个名为Effort的测试框架:https://entityframework-effort.net/

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

我通过使用 Linq 查询而不是单个成员解决了它:

    // 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://learn.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);
    }

感谢你们俩为我指出正确的方向(或至少远离问题的原因(。

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

要解决此问题,您可以从 DbContext 中提取一个接口并创建第二个"假"DbContext。这个 Fake 可能包含许多 FakeDbSet 类(继承 DbSet(。

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

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 中,所有这些都无关紧要,当然,您只需使用内存中数据库类型即可。

最新更新