如何使用Moq和Autofixture模拟Entity Framework 6

6

我正在使用AutoMoq,但由于Entity Framework(使用EF6和Code First),我的第一个单元测试写法有点困惑。

// in service class(constructor)
private readonly MyContext context;

public PriceService(MyContext context)
{
    this.context = context;
}

// following would be in nunit test method.
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var priceService = fixture.Create<PriceService>();

当我运行单元测试时,它会崩溃。
在Ploeh.AutoFixture.Kernel.TerminatingSpecimenBuilder.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.RecursionGuard.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.Postprocessor`1.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.RecursionGuard.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.Postprocessor`1.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)中获取第一个元素
在Ploeh.AutoFixture.Kernel.RecursionGuard.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.AutoPropertiesCommand`1.Execute(Object specimen, ISpecimenContext context)中执行
在Ploeh.AutoFixture.Kernel.Postprocessor`1.Create(Object request, ISpecimenContext context)中创建
在Ploeh.AutoFixture.Kernel.CompositeSpecimenBuilder.c__DisplayClass6.b__1(ISpecimenBuilder b)中创建
在System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()中移动
在System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()中移动
在System.Linq.Enumerable.d__a5`1.MoveNext()中移动
在System.Linq

编辑

在 EF 6 中,它似乎正在使 DbSet 更易于模拟。

https://entityframework.codeplex.com/wikipage?title=Design%20Meeting%20Notes%20-%20May%2016%2c%202013

使DbSet更容易模拟
这意味着添加受保护的构造函数并使方法虚拟化。
请注意,使用受保护的构造函数派生自DbSet的类型将创建一个未绑定到任何上下文的对象,并且方法将不起作用。从创建测试替身的角度来看,这使它非常类似于IDbSet。
如果我们采用此选项,我们可能会潜在地过时IDbSet。
值得注意的是,尚未确定任何情况下这与使用IDbSet作为测试替身没有功能上的差异。然而,社区普遍认为接口更受欢迎。
有人知道如何模拟它吗?
编辑2
我找到了this article但它一直崩溃。
public class MyContext : DbContext
{
    //public GroceryListContext()
    //    : base()
    //{

    //}
    public virtual DbSet<Price> Prices { get; set; }
}

[Test]
public void Test_Price_Object_Setup_Properly()
{
    var mockContext = new Mock<MyContext>();

    var mockSet = new Mock<DbSet<Price>>(); // had to add EF to my test solution.
    mockContext.Setup(m => m.Prices).Returns(mockSet.Object);
    var service = new PriceService(mockContext.Object);

    // dies when using autofixture so thought try first moq like in article
   //var priceService = fixture.Create<PriceService>();

   Assert.That(true, Is.EqualTo(false));
}

除以下情况外:

MyContext.Tests.Services.PriceServiceTests.Test_If_Price_Object_Setup_Properly: System.ArgumentException : 要模拟的类型必须是接口、抽象类或非密封类。 ----> System.TypeLoadException : 来自程序集“DynamicProxyGenAssembly2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=a621a9e7e5c32e69”的“DbSet1Proxyb409fc6b430b4568aac048b60ea2f02e”类型上的“Create”方法试图隐式覆盖具有更弱类型参数约束的方法。


1
问题:您是否已经确定 EF6 的行为与 5 有所不同,还是只是想做到完备?相关链接:https://dev59.com/N3LYa4cB1Zd3GeqPXmwY、https://dev59.com/OG3Xa4cB1Zd3GeqPdlu3 和 https://autofixture.codeplex.com/discussions/262557。 - Ruben Bartelink
你提供的最后一个例子,在没有AutoFixture的情况下在我的机器上可以运行。我复制了你的PriceService、MyContext类和Test_Price_Object_Setup_Properly方法。由于你没有提供,我实现了自己的Price类,并且它可以正常运行,没有任何异常。我正在使用Entity Framework 6.0 RC 1和Visual Studio 2013 RC。 - Olav Nybø
@RubenBartelink 好的,我的问题最初是如何模拟Datacontext,然后我发现EF可以在dbset上进行模拟,不需要包装器。在做教程时,我遇到了很大的问题。我现在知道问题出在autofixture上,它安装了moq的3.0版本,但是教程需要4.0版本才能正常工作。因此,我不确定现在是否还能使用autofixture。 - chobo2
@OlavNybø - 是的,现在我的机器上可以工作了,因为你可能刚刚直接从nuget包安装了moq。我从autofixture中获取它,并且那会安装版本3,但是为了让那段代码工作,你需要moq版本4。 - chobo2
@chobo 很高兴你现在已经解决了问题,但显然花费时间来达到这个结果并不好。一般来说,AutoFixture依赖于它需要通过测试的最早版本。如果你需要Moq v4,因为你有一个带有奇怪依赖关系的库。那么AutoFixture.Xunit是否需要1.9.2,因为1.8与Tech Z不兼容?换句话说,我认为AF不强制依赖于不是绝对必要的东西非常重要和有用(例如,3.0+对.NET 4的依赖最近让我花了将近一天的时间 - 但我有机会投票是否可以接受该依赖)。 - Ruben Bartelink
2个回答

4
您需要提供一个规范(Specification),指示应该模拟DbSet<T>类(尽管它不是抽象类型或接口)。
原因是因为DbSet<T>类是公共的,但它有一个protected构造函数。 规范:
internal class DbSetTypeSpecification : IRequestSpecification
{
    public bool IsSatisfiedBy(object request)
    {
        var type = request as Type;
        if (type == null)
            return false;

        return type.IsGenericType
            && typeof(DbSet<>) == type.GetGenericTypeDefinition();
    }
}

例子:

[Fact]
public void Test()
{
    var fixture = new Fixture();
    fixture.ResidueCollectors.Add(
        new MockRelay(
            new DbSetTypeSpecification()));

    Assert.DoesNotThrow(() => 
        fixture.Create<PriceService>());
}

现在AutoFixture可以提供自动生成的PriceService值。


请注意,MyContext类也是公共的,并且根据我的理解,它也有一个公共构造函数。这意味着AutoFixture默认不会为MyContext类提供自动模拟实例。

(如果您能提供您的场景,我可能能够提供进一步帮助。)


你尝试过在EF 6上使用吗?我发现原因是AutoFixture安装了moq 3,而我需要moq 4才能使教程正常工作。我尝试保留moq 4和autofixture,但我无法导入Fixture "using"。 - chobo2
我正在研究Autofixture,以前从未使用过。我想设置我的测试,这样我就不必手动创建所有的模拟对象(使用moq)和虚拟数据。这就是我想做的事情。当我需要在我的模拟中有特定的数据值时(比如PriceObject中的价格必须为5),我也有些不清楚。 - chobo2
@chobo2 关于 Moq,请阅读我的回答这里 - Nikos Baxevanis
Nikos,即使其他人对他们的点击更加珍惜,你也赢得了我的+1 :) @chobo2 NB请确保查看有关生成通过Moqed接口返回的测试数据的各种SO Q&A(Moq基本上不提供全局挂钩返回值生成的方法,因此您需要了解何时/如何/在哪里可以根据测试输入适当的自定义)。 (其中许多帖子通常涉及EF-它经常使人们停止思考正在隔离问题。始终确保测试失败的原因是正确的至关重要-特别是在自动模拟中) - Ruben Bartelink
@NikosBaxevanis - 是的,我看到了你的帖子,我找出了为什么它对我不起作用的原因。我告诉NuGet不要获取依赖项,其中包括AutoFixture,因此它只获取了AutoFixture.moq,这就是为什么名称空间没有导入的原因。 - chobo2

1

有一个名为AutoFixture.AutoEF的NuGet包可能会解决您的问题

fixture.Customize(new EntityCustomization(new DbContextEntityTypesProvider(typeof(MyContext))));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接