Moqing Enity Framework 6.使用DbSet <>包含()

Get*_*zzy 42 linq unit-testing entity-framework

我想提出这个问题的背景.如果你愿意,可以跳过.很长一段时间以来,我一直密切关注stackoverflow和其他地方关于代码测试的正在进行的辩论,因为它与EF有关.一个阵营说,由于Linq与Objects&Sql和实现之间的差异,直接针对数据库进行测试.另一个人说通过嘲弄测试.

另一个意见分歧是使用存储库或接受DbContext和DbSet已经提供工作单元和存储库模式的问题.在我使用EF的时候,我已经尝试过这些阵营提供的各种意见组合.无论我做了什么,EF证明很难测试.

我很高兴地发现EF团队使得DbSet在EF 6中更具可模仿性.他们还提供了有关如何模拟DbSet的文档,包括使用Moq的异步方法.在处理涉及Web Api的最新项目时,我意识到如果我可以模拟EF,我可以跳过编写存储库,因为编写它们的正常原因是使事情变得可测试.在阅读了一些博客文章后,灵感来了......

- 背景结束---

实际问题是,遵循EF团队提供的关于如何使用Moq DbSet的示例代码,如果在任何代码中使用.Include(),则抛出ArgumentNullException.

关于SO的其他相关帖子

这是我的DbContext界面:

public interface ITubingForcesDbContext
{
    DbSet<WellEntity> Wells { get; set; }

    int SaveChanges();

    Task<int> SaveChangesAsync();

    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
Run Code Online (Sandbox Code Playgroud)

这是我的控制器处理的主要实体

public class WellEntity
{
    public int Id { get; set; }
    public DateTime DateUpdated { get; set; }
    public String UpdatedBy { get; set; }

    [Required]
    public string Name { get; set; }
    public string Location { get; set; }

    public virtual Company Company { get; set; }

    public virtual ICollection<GeometryItem> GeometryItems
    {
        get { return _geometryItems ?? (_geometryItems = new Collection<GeometryItem>()); }
        protected set { _geometryItems = value; }
    }
    private ICollection<GeometryItem> _geometryItems;

    public virtual ICollection<SurveyPoint> SurveyPoints
    {
        get { return _surveyPoints ?? (_surveyPoints = new Collection<SurveyPoint>()); }
        protected set { _surveyPoints = value; }
    }
    private ICollection<SurveyPoint> _surveyPoints;

    public virtual ICollection<TemperaturePoint> TemperaturePoints
    {
        get { return _temperaturePoints ?? (_temperaturePoints = new Collection<TemperaturePoint>()); }
        protected set { _temperaturePoints = value; }
    }
    private ICollection<TemperaturePoint> _temperaturePoints;
}
Run Code Online (Sandbox Code Playgroud)

这是直接使用EF DbContext的控制器

 [Route("{id}")]
 public async Task<IHttpActionResult> Get(int id)
 {
        var query = await TheContext.Wells.
                                   Include(x => x.GeometryItems).
                                   Include(x => x.SurveyPoints).
                                   Include(x => x.TemperaturePoints).
                                   SingleOrDefaultAsync(x => x.Id == id);
        if (query == null)
        {
            return NotFound();
        }
        var model = ModelFactory.Create(query);
        return Ok(model);
}
Run Code Online (Sandbox Code Playgroud)

最后这是失败的测试......

测试设置---

   [ClassInitialize]
   public static void ClassInitialize(TestContext testContest)
        {

            var well1 = new WellEntity { Name = "Well 1" };
            var well2 = new WellEntity { Name = "Well 2" };
            var well3 = new WellEntity { Name = "Well 3" };
            var well4 = new WellEntity { Name = "Well 4" };

            well1.GeometryItems.Add(new GeometryItem());
            well1.TemperaturePoints.Add(new TemperaturePoint());
            well1.SurveyPoints.Add(new SurveyPoint());

            well2.GeometryItems.Add(new GeometryItem());
            well2.TemperaturePoints.Add(new TemperaturePoint());
            well2.SurveyPoints.Add(new SurveyPoint());

            well3.GeometryItems.Add(new GeometryItem());
            well3.TemperaturePoints.Add(new TemperaturePoint());
            well3.SurveyPoints.Add(new SurveyPoint());

            well4.GeometryItems.Add(new GeometryItem());
            well4.TemperaturePoints.Add(new TemperaturePoint());
            well4.SurveyPoints.Add(new SurveyPoint());

            var wells = new List<WellEntity> { well1, well2, well3, well4 }.AsQueryable();

            var mockWells = CreateMockSet(wells);

            _mockContext = new Mock<ITubingForcesDbContext>();
            _mockContext.Setup(c => c.Wells).Returns(mockWells.Object);
   }

   private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data) where T : class
    {
        var mockSet = new Mock<DbSet<T>>();

        mockSet.As<IDbAsyncEnumerable<T>>()
            .Setup(m => m.GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));

        mockSet.As<IQueryable<T>>()
               .Setup(m => m.Provider)
               .Returns(new TestDbAsyncQueryProvider<T>(data.Provider));

        mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<T>>().Setup(m =>m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<T>>().Setup(m=>m.GetEnumerator()).
        Returns(data.GetEnumerator());

        return mockSet;
   }

  [TestMethod]  
   public async Task Get_ById_ReturnsWellWithAllChildData()
    {
        // Arrange
        var controller = new WellsController(_mockContext.Object);

        // Act
        var actionResult = await controller.Get(1);

        // Assert
        var response = actionResult as OkNegotiatedContentResult<WellModel>;
        Assert.IsNotNull(response);
        Assert.IsNotNull(response.Content.GeometryItems);
        Assert.IsNotNull(response.Content.SurveyPoints);
        Assert.IsNotNull(response.Content.TemperaturePoints);
   }
Run Code Online (Sandbox Code Playgroud)

TestDbAsyncQueryProvider和TestDbAsyncEnumerator直接来自引用的EF团队文档.我已经尝试了几种不同的变体,我如何为模拟创建数据,没有任何运气.

Joh*_*man 39

对于那些对如何解决NSubstitute和Entity Framework 6+.Include("Foo")问题而感兴趣的人,我能够Include通过以下方式绕过我的电话:

var data = new List<Foo>()
{
    /* Stub data */
}.AsQueryable();

var mockSet = Substitute.For<DbSet<Foo>, IQueryable<Foo>>();
((IQueryable<Post>)mockSet).Provider.Returns(data.Provider);
((IQueryable<Post>)mockSet).Expression.Returns(data.Expression);
((IQueryable<Post>)mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Post>)mockSet).GetEnumerator().Returns(data.GetEnumerator());

// The following line bypasses the Include call.
mockSet.Include(Arg.Any<string>()).Returns(mockSet);
Run Code Online (Sandbox Code Playgroud)

  • 对不起,我正在使用Moq.但是,你理解正确.我使用的并且对我有效的行是:`dbSet.Setup(m => m.Include(It.IsAny <String>())).返回(dbSet.Object);`where`dbSet = new Mock <DbSet <T >>();`也许我没有投射到IQueryable的原因是相关的. (14认同)
  • 您可以使用替换"Bar"与It.IsAny <String>()来使其更通用.然后你可以调用.Include(x => x.Bar),因为它在内部调用.Include("Bar") (7认同)
  • 这对我来说实际上并没有用,但也许我并没有完全跟随你.你的意思是使用`.Include(Args.Any <string>())`代替`.Include("Bar")`,因为这对我不起作用:( (2认同)
  • 不,这不起作用.在设置它时,它会抛出一个System.NotSupportedException,表示Expression引用了一个不属于模拟对象的方法.关于如何解决这个问题并让它运作的任何想法? (2认同)

Bre*_*ett 32

这是一个使用Moq的完整示例.您可以将整个示例粘贴到单元测试类中.感谢@ jbaum012和@Skuli的评论.我还推荐微软的优秀教程.

// An Address entity
public class Address
{
    public int Id { get; set; }
    public string Line1 { get; set; }
}

// A Person referencing Address
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Address Address { get; set; }
}

// A DbContext with persons and devices
// Note use of virtual (see the tutorial reference)
public class PersonContext : DbContext
{
    public virtual DbSet<Person> Persons { get; set; }
    public virtual DbSet<Address> Addresses { get; set; }
}

// A simple class to test
// The dbcontext is injected into the controller
public class PersonsController
{
    private readonly PersonContext _personContext;

    public PersonsController(PersonContext personContext)
    {
        _personContext = personContext;
    }

    public IEnumerable<Person> GetPersons()
    {
        return _personContext.Persons.Include("Address").ToList();
    }
}

// Test the controller above
[TestMethod]
public void GetPersonsTest()
{
    var address = new Address { Id = 1, Line1 = "123 Main St." };
    var expectedPersons = new List<Person>
    {
        new Person { Id = 1, Address = address, Name = "John" },
        new Person { Id = 2, Address = address, Name = "John Jr." },
    };

    var mockPersonSet = GetMockDbSet(expectedPersons.AsQueryable());
    mockPersonSet.Setup(m => m.Include("Address")).Returns(mockPersonSet.Object);

    var mockPersonContext = new Mock<PersonContext>();
    mockPersonContext.Setup(o => o.Persons).Returns(mockPersonSet.Object);

    // test the controller GetPersons() method, which leverages Include()
    var controller = new PersonsController(mockPersonContext.Object);
    var actualPersons = controller.GetPersons();
    CollectionAssert.AreEqual(expectedPersons, actualPersons.ToList());
}

// a helper to make dbset queryable
private Mock<DbSet<T>> GetMockDbSet<T>(IQueryable<T> entities) where T : class
{
    var mockSet = new Mock<DbSet<T>>();
    mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(entities.Provider);
    mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(entities.Expression);
    mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(entities.ElementType);
    mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(entities.GetEnumerator());
    return mockSet;
}
Run Code Online (Sandbox Code Playgroud)

  • 为我做的技巧的特定行是mockPersonSet.Setup(m => m.Include("Address")).返回(mockPersonSet.Object); (10认同)
  • 我用 EntityFrameworkCore 2.2.6 尝试了此操作,并收到一条错误,指出我无法模拟 DbSet 的“Include”扩展方法。 (2认同)

Pau*_*bra 8

玩这个并在这里引用答案设置调用扩展方法的结果看起来像Moq不能模拟静态扩展方法

我试着添加:

mockSet.Setup(t => t.FirstAsync()).Returns(Task.FromResult(data.First()));
mockSet.Setup(t => t.FirstAsync(It.IsAny<Expression<Func<T, bool>>>())).Returns(Task.FromResult(data.First()));
Run Code Online (Sandbox Code Playgroud)

而且Moq抱怨说:

System.NotSupportedException:Expression引用不属于模拟对象的方法:t => t.FirstAsync()

所以似乎有三种选择:

  1. 重构您的代码以进一步隔离dbcontext,因此您不必测试此行为
  2. 从DbSet切换到IDbSet而不是模拟DbContext
  3. 允许您的测试创建SQL压缩数据库并使用数据填充它以运行测试

  • 对于迟到这个讨论的人.这里提供的解决方案:http://entityframework.codeplex.com/SourceControl/latest#test/EntityFramework/FunctionalTests/TestDoubles/MockableDbSetTests.cs使用`Include`重载,它接受`string`而不是lambda重载. (9认同)
  • 嗨保罗,还有一个注意事项,来自EF团队的人回到我身边,这并不明显,但显然这些事情可以被嘲笑......看看EF FunctionalTests项目中MockableDbSetTests中的"Moq_DbSet_can_be_used_for_query_with_Include_extension_method_that_does_something".谢谢,亚瑟 (4认同)