使用Moq模拟Entity Framework 6的ObjectResult

3
我该如何使用Moq模拟Entity Framework 6中的ObjectResult,以便我可以对依赖于EF数据库连接的代码进行单元测试?
阅读了许多类似的问题和答案,并从中获得了许多有用的信息,我实现了一个我认为相当优雅的解决方案,并觉得应该分享一下,因为这里的社区帮助我达到了这个目标。因此,我将继续回答这个问题,并可能遭受一些嘲笑(意在玩笑)。
2个回答

6
首先,ObjectResult没有公共的无参构造函数,因此需要先创建一个可测试的ObjectResult包装器。这篇文章中@forsvarir (https://stackoverflow.com/users/592182/forsvarir) 的答案让我正确地思考了这些问题(EF6 - 无法为单元测试模拟ObjectResult<T>的返回值):
using System.Data.Entity.Core.Objects;

namespace MyNamespace.Mocks
{
    public class TestableEfObjectResult<T> : ObjectResult<T> { }
}

当然,需要模拟 DbContext。然后设置您的方法以返回适当的模拟枚举器。为了方便起见,我创建了一个方法来帮助创建 Mock EF Results,以避免我的测试代码变得混乱和冗余。这可以存在于您为测试拥有的某些实用类中,尽管我在此处仅将其包含为私有方法。关键是,当调用 GetEnumerator 时,模拟对象结果需要返回枚举器:

namespace MyNamespace.Mocks
{
    public class MockSomeDbEntities
    {
        public static Mock<SomeDbEntities> Default
        {
            get
            {
                var mockSomeDbEntities = new Mock<SomeDbEntities>();

                mockSomeDbEntities
                  .Setup(e => e.SomeMethod(It.IsAny<int>()))
                  .Returns(MockEfResult(Enumerators.SomeCollection).Object);

                return mockSomeDbEntities;
            }
        }

        private static Mock<TestableEfObjectResult<T>> MockEfResult<T>(Func<IEnumerator<T>> enumerator) where T : class 
        {
            var mock = new Mock<TestableEfObjectResult<T>>();
            mock.Setup(m => m.GetEnumerator()).Returns(enumerator);
            return mock;
        }
    }
}

我创建的 Enumerators 类用于在调用模拟函数时回传枚举器,其代码如下。在此示例中,我让仿造的枚举器创建了 5 行数据:
using System;
using System.Collections.Generic;

namespace MyNamespace.FakeData
{
    public static class Enumerators
    {
        public static IEnumerator<Some_Result> SomeCollection()
        {
            yield return FakeSomeResult.Create(1);
            yield return FakeSomeResult.Create(2);
            yield return FakeSomeResult.Create(3);
            yield return FakeSomeResult.Create(4);
            yield return FakeSomeResult.Create(5);
        }
    }
}

正如您所看到的,这只是依靠一个创建每个虚假数据行的类:

namespace MyNamespace.FakeData
{
    public static class FakeSomeResult
    {
        public static Some_Result Create(int id)
        {
            return new Some_Result
            {
                Id = id,
            };
        }
    }
}

能够在这个层次上进行模拟真正提高了我进行BDD的能力,只需要模拟或伪造外围部分,而不需要模拟或伪造我的代码,因此我可以获得更完整的测试覆盖率。
希望这能帮助那些像我一样正在寻找一种干净简洁的方式来模拟Entity Framework 6的人。

1

我遇到过这个问题很多次

我的解决方法是模拟ObjectResult的GetEnumeartor方法。这可以在测试方法中轻松完成,开销非常小。

例如

假设我们有一个repo方法,调用一个返回ObjectResult<string>的DBContext方法。 repo方法将通过枚举ObjectResult<string>来获取结果值string,例如objResult.FirstOrDefault()。正如您所知,LINQ方法只是调用ObjectResult的enumerator。因此,模拟ObjectResult的GetEnumeartor()函数就可以解决问题。任何结果的枚举都将返回我们的模拟枚举器。

例子


这里是一个简单的函数,它可以从一个字符串中产生一个IEnumerator。这个函数可以与匿名方法一起内联使用,但是为了说明目的,在这里使用它会使代码更难读懂。
var f = new Func< string,IEnumerator< string> >( s => ( ( IEnumerable< string > )new []{ s } ).GetEnumerator( ) );

这个函数可以通过传递类似于这样的字符串来调用。
var myObjResultReturnEnum = f( "some result" );

这可能会让我们更容易获得我们期望的ObjectResult<string>GetEnumerator()方法返回的IEnumerator<string>,因此调用ObjectResult的任何repo方法都将接收到我们的字符串。
// arrange
const string shouldBe = "Hello World!";
var objectResultMock = new Mock<ObjectResult<string>>();
objectResultMock.Setup( or => or.GetEnumerator() ).Returns(() => myObjResultReturnEnum );

现在我们有一个模拟的ObjectResult<string>,我们可以将其传递到存储库方法中,推断出我们的方法最终将以某种方式调用枚举器,并获取我们的shouldBe值。
var contextMock = new Mock<SampleContext>( );
contextMock.Setup( c => c.MockCall( ) ).Returns( ( ) => objectResultMock.Object );

// act
var repo = new SampleRepository( contextMock.Object );
var result = repo.SampleSomeCall( );

// assert
Assert.IsNotNull(result);
Assert.AreEqual(shouldBe, result);

不错,我得试试那种风格。 - Jim Speaker

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