因此,EntityFramework 6比以前的版本更易于测试。对于像Moq这样的框架,互联网上有some nice examples,但情况是,我更喜欢使用NSubstitute。我已经将“非查询”示例翻译为使用NSubstitute,但我无法理解“查询测试”。
Moq的items.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider);
如何转化为NSubstitute?我认为像((IQueryable<T>) items).Provider.Returns(data.Provider);
这样的东西但是没有用。我也试过了items.AsQueryable().Provider.Returns(data.Provider);
,但这也不起作用。
我得到的例外是:
“System.NotImplementedException:成员'IQueryable.Provider' 尚未在类型'DbSet
1Proxy' which inherits from 'DbSet
1'上实现。 'DbSet`1'的测试双精度必须提供 使用的方法和属性。“
因此,让我引用上面链接中的代码示例。此代码示例使用Moq来模拟DbContext和DbSet。
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = new Mock<DbSet<Blog>>();
mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(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());
var mockContext = new Mock<BloggingContext>();
mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
// ...
}
这就是我与NSubstitute的距离
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<DbSet<Blog>>();
// it's the next four lines I don't get to work
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// ...
}
所以问题是;如何替换IQueryable的属性(如Provider)?
答案 0 :(得分:37)
这是因为NSubstitute语法特定。例如:
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
NSubstitute调用Provider的getter,然后指定返回值。这个getter调用没有被替换者截获,你得到一个例外。这是因为在DbQuery类中显式实现了IQueryable.Provider属性。
您可以使用NSub显式创建多个接口的替代,并创建一个涵盖所有指定接口的代理。然后,替换者将拦截对接口的调用。 请使用以下语法:
// Create a substitute for DbSet and IQueryable types:
var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();
// And then as you do:
((IQueryable<Blog>)mockSet).Provider.Returns(data.Provider);
...
答案 1 :(得分:17)
感谢Kevin,我在代码翻译中发现了问题。
unittest code samples正在模拟DbSet
,但NSubstitute需要接口实现。因此,NSubstitute的Moqs new Mock<DbSet<Blog>>()
相当于Substitute.For<IDbSet<Blog>>()
。您并不总是需要提供界面,这就是为什么我感到困惑。但在这个具体案例中,结果证明是至关重要的。
事实证明,在使用接口IDbSet时我们不必强制转换为Queryable。
所以工作测试代码:
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<IDbSet<Blog>>();
mockSet.Provider.Returns(data.Provider);
mockSet.Expression.Returns(data.Expression);
mockSet.ElementType.Returns(data.ElementType);
mockSet.GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// Act and Assert ...
}
我已经编写了一个小的扩展方法来清理单元测试的Arrange部分。
public static class ExtentionMethods
{
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
return dbSet;
}
}
// usage like:
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
不是问题,但如果您还需要能够支持异步操作:
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
if (dbSet is IDbAsyncEnumerable)
{
((IDbAsyncEnumerable<T>) dbSet).GetAsyncEnumerator()
.Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
dbSet.Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
}
return dbSet;
}
// create substitution with async
var mockSet = Substitute.For<IDbSet<Blog>, IDbAsyncEnumerable<Blog>>().Initialize(data);
// create substitution without async
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
答案 2 :(得分:4)
这是我生成假DbSet的静态通用静态方法。它可能很有用。
public static class CustomTestUtils
{
public static DbSet<T> FakeDbSet<T>(List<T> data) where T : class
{
var _data = data.AsQueryable();
var fakeDbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
((IQueryable<T>)fakeDbSet).Provider.Returns(_data.Provider);
((IQueryable<T>)fakeDbSet).Expression.Returns(_data.Expression);
((IQueryable<T>)fakeDbSet).ElementType.Returns(_data.ElementType);
((IQueryable<T>)fakeDbSet).GetEnumerator().Returns(_data.GetEnumerator());
fakeDbSet.AsNoTracking().Returns(fakeDbSet);
return fakeDbSet;
}
}
答案 3 :(得分:2)
我在一年前写了一个包装器,围绕你从Testing with Your Own Test Doubles (EF6 onwards)引用的相同代码。这个包装器可以在GitHub DbContextMockForUnitTests找到。此包装器的目的是减少设置单元测试所需的重复/重复代码的数量,这些单元测试使用EF来模拟DbContext
和DbSets
。您在OP中拥有的大多数模拟EF代码可以减少到2行代码(,如果使用DbContext.Set<T>
而不是DbSet属性,则只有1行)并且模拟代码是然后在包装器中调用。
要使用它,请将文件夹MockHelpers
中的文件复制并包含在Test项目中。
以下是使用上述内容的示例测试,请注意,现在只需要2行代码就可以在模拟的DbSet<T>
上设置模拟DbContext
。
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSet();
mockContext.Blogs.Returns(mockSet);
// act
}
使用.ToListAsync()
上的DbSet<T>
等异步/等待模式调用某些东西也很容易。
public async Task GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSetForAsync(); // only change is the ForAsync version of the method
mockContext.Blogs.Returns(mockSet);
// act
}
答案 4 :(得分:0)
你不应该嘲笑IQueryable的所有部分。当我使用NSubstitute来模拟EF DbContext时,我会这样做:
interface IContext
{
IDbSet<Foo> Foos { get; set; }
}
var context = Substitute.For<IContext>();
context.Foos.Returns(new MockDbSet<Foo>());
在列表周围使用IDbSet的简单实现或我的MockDbSet()的实现。
一般来说,你应该模拟接口,而不是类型,因为NSubstitute只会覆盖虚方法。