我正在编写一个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);
我还没有完全掌握模拟测试,并且不确定为什么会失败。谁能解释并提供解决方案?
答案 0 :(得分:3)
从我记得以这种方式模拟实体框架非常困难,我建议,如果您非常坚持以这种方式测试框架,那么最好将上下文包装在接口IBookContext
中,然后您可以使用自己的方法来包装实体框架的功能,从而使事情更容易实现,并且您不必处理实体框架。
如果您使用的是.Net core,则可以使用一个InMemory提供程序:https://docs.microsoft.com/en-us/ef/core/providers/in-memory/
如果您使用的是框架,那么会有一个名为Effort的测试框架:https://entityframework-effort.net/
两者都在实体框架的内存实现中-您可以在测试中使用它们,因此不必与数据库集成(速度很慢)
答案 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中,所有这些都不相关,您当然可以只使用内存数据库类型。