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