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

我想提出这个问题的背景。 跳过,如果你喜欢。 相当长一段时间以来,我一直密切关注正在进行的有关testing代码的stackoverflow和其他地方的辩论,因为它涉及到EF。 一个阵营说,由于Linq到Objects&Sql和实现之间的差异,直接针对数据库进行testing。 另一个说嘲笑testing。

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

我非常兴奋地发现EF团队使得DbSet在EF 6中更加可嘲弄。他们还提供了关于如何模拟DbSet的文档 ,包括使用Moq的asynchronous方法。 在参与我最新的涉及Web Api的项目时,我意识到,如果我可以模拟EF,那么我可以跳过编写知识库,因为写这些知识库的正常原因是为了使testing成为可能。 阅读了一些这样的博客文章后,灵感来了…

– 背景结束—

实际的问题是,遵循EF小组关于如何使用Moq DbSet的示例代码,如果在任何代码中使用.Include(),则会引发ArgumentNullException。

SO上的其他相关post

这是我的DbContext接口:

public interface ITubingForcesDbContext { DbSet<WellEntity> Wells { get; set; } int SaveChanges(); Task<int> SaveChangesAsync(); Task<int> SaveChangesAsync(CancellationToken cancellationToken); } 

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

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

这里是直接使用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); } 

最后这里是失败的testing…

testing设置—

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

TestDbAsyncQueryProvider&TestDbAsyncEnumerator直接来自引用的EF小组文档。 我已经尝试了几种不同的变体来创build模拟数据,没有任何运气。

对于任何人在这个问题上.Include("Foo")问题,关心如何解决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); 

这是一个使用Moq的完整例子。 您可以将整个示例粘贴到unit testing课程中。 感谢@ 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; } 

玩这个和在这里引用答案安装结果调用扩展方法它看起来像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())); 

Moq抱怨说:

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

所以看来有三个select:

  1. 重构你的代码来进一步隔离dbcontext,所以你不必testing这种行为
  2. 从DbSet切换到IDbSet而不是模拟DbContext
  3. 允许你的testing创build一个SQL压缩数据库并用数据填充它来运行你的testing

EF团队提供的示例DbSet就是这样一个例子。

如果你想嘲笑Include (或FindAsync ) ,你必须自己动手。