2014-01-12 146 views
29

所以EntityFramework 6比以前的版本更好地测试。互联网上有像Moq这样的框架的some nice examples,但情况是,我更喜欢使用NSubstitute。我有翻译过的“非查询”例子与NSubstitute的使用一起工作,但我无法理解“查询测试”。NSubstitute DbSet/IQueryable <T>

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);,但那也没用。

我得到的exeption是:

“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

更新:使用[EntityFramework.Testing.NSubstitute](https://github.com/scott-xu/EntityFramework.Testing)包,提供DbAsyncQueryProvider的实现。 –

回答

34

这是因为特定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); 
... 
+0

谢谢!这是非常有用的信息。我不确定如果这是替代DbSet的最佳方式(因为它使用IDbSet解决了问题),但它确实在其他情况下很有用。 –

+0

我将你标记为答案,因为你的建议是支持Async(Substitute.For ,IDbAsyncEnumerable >())的解决方案。这不是真正的问题,而是它的延伸。再次感谢您的洞察! –

+4

提示:确保你的上下文中的'DbSet'是'virtual' –

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只会覆盖虚拟方法。

+0

在EF 6中,对于可测试性,DbSet属性应该是虚拟的。我不想创建一个假的DbSet包装。取代这4个属性应该比创建你提出的包装更容易和更好。所以你的建议并不是我正在寻找的答案。 –

+1

根据错误信息判断,我会说由NSubstitute创建的DbSet代理不支持明确实现的接口,只支持您替换的特定合同或对象。这在(https://github.com/nsubstitute/NSubstitute/issues/95)中就已经出现了,并且可能是框架的一个限制。 – Kevin

15

感谢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 ... 
} 

我写了一个小extention方法清理单元测试的排列部分。

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); 
+0

没关系。但什么是TestDbAsyncEnumerator和TestDbAsyncQueryProvider。 – cosset

+0

这是一个内存数据库查询提供程序,正如我在我的问题中提供的链接所解释的,以及此答案。看看在MSDN页面:http://msdn.microsoft.com/nl-nl/data/dn314429.aspx#async –

+2

我试过这个,但得到一个异常:NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException:无法返回值为BloggingContext.get_Blogs(预期类型DbSet'1)键入IDbSet'1Proxy。上下文是由EF TT模板生成的 – Shevek

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; 
    } 

} 
+0

我认为从参数“data”的定义中缺少关键字“this” –

2

我写了一个大约一年前围绕相同的代码,你从Testing with Your Own Test Doubles (EF6 onwards)引用的包装。这个包装可以在GitHub DbContextMockForUnitTests找到。这个包装的目的是为了减少设置单元测试所需的重复/重复代码的数量,这些单元测试使用EF来模拟DbContextDbSets。您在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 
} 

这也很容易让这个调用一些使用上的DbSet<T>异步/ AWAIT像.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 
}