NSubstitute DbSet / IQueryable<T>
Posted
技术标签:
【中文标题】NSubstitute DbSet / IQueryable<T>【英文标题】: 【发布时间】:2014-01-30 22:35:36 【问题描述】:因此,EntityFramework 6 的可测试性比以前的版本要好得多。互联网上有 some nice examples 用于 Moq 等框架,但情况是,我更喜欢使用 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)?
【问题讨论】:
更新:使用提供 DbAsyncQueryProvider 实现的 EntityFramework.Testing.NSubstitute 包。 【参考方案1】:发生这种情况是因为 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);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());
【讨论】:
谢谢!这是非常有用的信息。我还不确定这是否是替代 DbSet 的最佳方法(事实证明,使用 IDbSet 可以解决问题),但它在其他情况下肯定会有所帮助。 我将您标记为答案,因为您的建议是支持异步的解决方案(Substitute.ForDbSet
s 在您的上下文中是 virtual
有处理.Include()
问题的优雅解决方案吗?我不断收到source is null
错误。【参考方案2】:
感谢 Kevin,我在我的代码翻译中发现了问题。
unittest code samples 正在模拟 DbSet
,但 NSubstitute 需要接口实现。因此,NSubstitute 的起订量 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 ...
我编写了一个小扩展方法来清理单元测试的排列部分。
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);
【讨论】:
没关系。但是TestDbAsyncEnumerator和TestDbAsyncQueryProvider是什么。 这是一个内存数据库查询提供程序,正如我在我的问题中提供的链接以及这个答案所解释的那样。看看msdn页面:msdn.microsoft.com/nl-nl/data/dn314429.aspx#async 我已经尝试过了,但是得到了一个异常:NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException: Can not return value of type IDbSet1Proxy for BloggingContext.get_Blogs (expected type DbSet
1)。上下文由 EF TT 模板生成
@Shevek 有同样的问题,你可以通过调整for调用中的接口/类型使其工作:Substitute.For<IDbSet<DataModel>, DbSet<DataModel>>();
@Shevek 你也可以修改你的 BloggingContext 类:public virtual IDbSet<Blog> Blogs get; set; public virtual IDbSet<Post> Posts get; set;
【参考方案3】:
这是我生成假 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;
【讨论】:
我认为参数“data”的定义中缺少关键字“this” 在几个项目中使用了这个。我喜欢它从我的测试中移除管道【参考方案4】:大约一年前,我围绕您从Testing with Your Own Test Doubles (EF6 onwards) 引用的相同代码编写了一个包装器。这个包装可以在GitHub DbContextMockForUnitTests 上找到。此包装器的目的是减少设置单元测试所需的重复/重复代码的数量,这些单元测试使用您要模拟 DbContext
和 DbSets
的 EF。您在 OP 中拥有的大多数模拟 EF 代码可以减少到 2 行代码(如果您使用 DbContext.Set<T>
而不是 DbSet 属性,则只有 1 行),然后调用模拟代码在包装器中。
要使用它,请将文件夹 MockHelpers
中的文件复制并包含到您的测试项目中。
这是一个使用上述内容的示例测试,请注意现在只需要 2 行代码即可在模拟的 DbContext
上设置模拟 DbSet<T>
。
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
让它成为一个调用使用 async/await 模式的测试同样容易,例如 DbSet<T>
上的 .ToListAsync()
。
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
【讨论】:
【参考方案5】:您不需要模拟 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 只会覆盖虚拟方法。
【讨论】:
在 EF 6 DbSet 中,应将属性设为虚拟以实现可测试性。我不想创建一个假的 DbSet 包装器。替换这 4 个属性应该比创建您提议的包装器更容易和更好。所以你的建议并不是我想要的答案。 从错误消息来看,我会说 NSubstitute 创建的 DbSet 代理不支持显式实现的接口,仅支持您要替换的特定合同或对象。这是在 (github.com/nsubstitute/NSubstitute/issues/95) 中诞生的,可能是框架的限制。以上是关于NSubstitute DbSet / IQueryable<T>的主要内容,如果未能解决你的问题,请参考以下文章
NSubstitute ForPartsOf模拟除一个以外的所有方法?