使用模拟框架进行测试Testing with a mocking framework

备注

仅限 EF6 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 6。 如果使用的是早期版本,则部分或全部信息不适用。

编写应用程序测试时,通常需要避免数据库的命中。 实体框架使你可以通过创建上下文来实现此目的,该上下文具有测试定义的行为,从而利用内存中数据。

用于创建测试双精度的选项Options for creating test doubles

可使用两种不同的方法创建上下文的内存中版本。

  • 创建自己的测试双精度型–此方法涉及编写您自己的上下文和 dbset 的内存中实现。 这使你可以很好地控制类的行为,但可能涉及到编写和拥有合理的代码量。
  • 使用模拟框架创建测试双精度型–使用模拟框架(如 Moq),可以在运行时为你的上下文提供内存中实现并在运行时动态创建。

本文将介绍如何使用模拟框架。 若要创建自己的测试,请参阅采用自己的测试进行测试双精度

为了演示如何将 EF 与模拟框架结合使用,我们将使用 Moq。 获取 Moq 的最简单方法是从 NuGet 安装 Moq 包

用预 EF6 版本测试Testing with pre-EF6 versions

本文中所述的方案取决于我们对 EF6 中的 DbSet 进行的一些更改。 若要使用 EF5 和更早的版本进行测试,请参阅使用虚设上下文进行测试

EF 内存中测试的限制双精度Limitations of EF in-memory test doubles

内存中测试双精度型可以是一种提供使用 EF 的应用程序的单元测试级别的好方法。 但是,在执行此操作时,将使用 LINQ to Objects 对内存中数据执行查询。 这可能会导致不同的行为,而不是使用 EF 的 LINQ 提供程序(LINQ to Entities)将查询转换为对数据库运行的 SQL。

这种差异的一个示例就是加载相关数据。 如果创建一系列博客,其中每个博客都有相关的文章,则在使用内存中数据时,将始终为每个博客加载相关文章。 但是,在对数据库运行时,仅当使用 Include 方法时才会加载数据。

出于此原因,建议始终包括某个级别的端到端测试(除了单元测试),以确保应用程序能够对数据库正常运行。

与本文一起介绍Following along with this article

本文提供了完整的代码清单,你可以将其复制到 Visual Studio 中,以根据你的需要进行。 最简单的方法是创建一个单元测试项目,并需要将 .NET Framework 4.5作为目标来完成使用 async 的部分。

EF 模型The EF model

要测试的服务利用的是由 “Bloggingcontext” 和博客和 Post 类组成的 EF 模型。 此代码可能是由 EF 设计器生成的,或是 Code First 模型中。

  1. using System.Collections.Generic;
  2. using System.Data.Entity;
  3. namespace TestingDemo
  4. {
  5. public class BloggingContext : DbContext
  6. {
  7. public virtual DbSet<Blog> Blogs { get; set; }
  8. public virtual DbSet<Post> Posts { get; set; }
  9. }
  10. public class Blog
  11. {
  12. public int BlogId { get; set; }
  13. public string Name { get; set; }
  14. public string Url { get; set; }
  15. public virtual List<Post> Posts { get; set; }
  16. }
  17. public class Post
  18. {
  19. public int PostId { get; set; }
  20. public string Title { get; set; }
  21. public string Content { get; set; }
  22. public int BlogId { get; set; }
  23. public virtual Blog Blog { get; set; }
  24. }
  25. }

虚拟 DbSet 属性与 EF 设计器Virtual DbSet properties with EF Designer

请注意,上下文中的 DbSet 属性标记为虚拟。 这将允许模拟框架从我们的上下文派生,并使用模拟实现覆盖这些属性。

如果使用 Code First,则可以直接编辑类。 如果使用的是 EF 设计器,则需要编辑生成上下文的 T4 模板。 打开><model_name。Context.tt 文件嵌套在 edmx 文件下,查找以下代码片段,并将其添加到 virtual 关键字中,如下所示。

  1. public string DbSet(EntitySet entitySet)
  2. {
  3. return string.Format(
  4. CultureInfo.InvariantCulture,
  5. "{0} virtual DbSet\<{1}> {2} {{ get; set; }}",
  6. Accessibility.ForReadOnlyProperty(entitySet),
  7. _typeMapper.GetTypeName(entitySet.ElementType),
  8. _code.Escape(entitySet));
  9. }

要测试的服务Service to be tested

为了演示使用内存中测试的测试,我们将为 BlogService 编写一些测试。 该服务可以创建新的博客(AddBlog)并返回按名称排序的所有博客(GetAllBlogs)。 除了 GetAllBlogs 之外,我们还提供了一个方法,该方法将异步获取按名称(GetAllBlogsAsync)排序的所有博客。

  1. using System.Collections.Generic;
  2. using System.Data.Entity;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. namespace TestingDemo
  6. {
  7. public class BlogService
  8. {
  9. private BloggingContext _context;
  10. public BlogService(BloggingContext context)
  11. {
  12. _context = context;
  13. }
  14. public Blog AddBlog(string name, string url)
  15. {
  16. var blog = _context.Blogs.Add(new Blog { Name = name, Url = url });
  17. _context.SaveChanges();
  18. return blog;
  19. }
  20. public List<Blog> GetAllBlogs()
  21. {
  22. var query = from b in _context.Blogs
  23. orderby b.Name
  24. select b;
  25. return query.ToList();
  26. }
  27. public async Task<List<Blog>> GetAllBlogsAsync()
  28. {
  29. var query = from b in _context.Blogs
  30. orderby b.Name
  31. select b;
  32. return await query.ToListAsync();
  33. }
  34. }
  35. }

测试非查询方案Testing non-query scenarios

这就是开始测试非查询方法所需的所有操作。 以下测试使用 Moq 创建上下文。 然后,它将创建一个 DbSet<博客> 并向其线路,使其从上下文的博客属性返回。 接下来,使用上下文创建新的 BlogService,然后使用 AddBlog 方法创建新的博客。 最后,测试将验证该服务是否在上下文中添加了新的博客并调用了 SaveChanges。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. using Moq;
  3. using System.Data.Entity;
  4. namespace TestingDemo
  5. {
  6. [TestClass]
  7. public class NonQueryTests
  8. {
  9. [TestMethod]
  10. public void CreateBlog_saves_a_blog_via_context()
  11. {
  12. var mockSet = new Mock<DbSet<Blog>>();
  13. var mockContext = new Mock<BloggingContext>();
  14. mockContext.Setup(m => m.Blogs).Returns(mockSet.Object);
  15. var service = new BlogService(mockContext.Object);
  16. service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");
  17. mockSet.Verify(m => m.Add(It.IsAny<Blog>()), Times.Once());
  18. mockContext.Verify(m => m.SaveChanges(), Times.Once());
  19. }
  20. }
  21. }

测试查询方案Testing query scenarios

为了能够对 DbSet 测试执行查询,我们需要设置 IQueryable 的实现。 第一步是创建一些内存中数据,我们使用的是博客><列表。 接下来,我们将创建一个上下文和 DBSet<博客> 然后将 DbSet 的 IQueryable 实现连接在一起–它们只是委托给适用于 List的 LINQ to Objects 提供程序。

然后,可以根据我们的测试来创建 BlogService,并确保从 GetAllBlogs 返回的数据按名称排序。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. using Moq;
  3. using System.Collections.Generic;
  4. using System.Data.Entity;
  5. using System.Linq;
  6. namespace TestingDemo
  7. {
  8. [TestClass]
  9. public class QueryTests
  10. {
  11. [TestMethod]
  12. public void GetAllBlogs_orders_by_name()
  13. {
  14. var data = new List<Blog>
  15. {
  16. new Blog { Name = "BBB" },
  17. new Blog { Name = "ZZZ" },
  18. new Blog { Name = "AAA" },
  19. }.AsQueryable();
  20. var mockSet = new Mock<DbSet<Blog>>();
  21. mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  22. mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  23. mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  24. mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
  25. var mockContext = new Mock<BloggingContext>();
  26. mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
  27. var service = new BlogService(mockContext.Object);
  28. var blogs = service.GetAllBlogs();
  29. Assert.AreEqual(3, blogs.Count);
  30. Assert.AreEqual("AAA", blogs[0].Name);
  31. Assert.AreEqual("BBB", blogs[1].Name);
  32. Assert.AreEqual("ZZZ", blogs[2].Name);
  33. }
  34. }
  35. }

用异步查询进行测试Testing with async queries

实体框架6引入了一组扩展方法,这些方法可用于以异步方式执行查询。 这些方法的示例包括 ToListAsync、FirstAsync、ForEachAsync 等。

由于实体框架查询使用 LINQ,因此扩展方法是在 IQueryable 和 IEnumerable 上定义的。 但是,因为它们仅设计为与实体框架一起使用,所以如果尝试在不是实体框架查询的 LINQ 查询中使用它们,则可能会收到以下错误:

源 IQueryable 不实现 IDbAsyncEnumerable{0}。 仅实现 IDbAsyncEnumerable 的源可用于实体框架异步操作。 有关详细信息,请参阅http://go.microsoft.com/fwlink/?LinkId=287068

尽管异步方法仅在针对 EF 查询运行时受支持,但在对 DbSet 的内存中测试双精度运行时,您可能需要在单元测试中使用它们。

若要使用异步方法,我们需要创建内存中的 DbAsyncQueryProvider 以处理异步查询。 尽管可以使用 Moq 设置查询提供程序,但使用代码创建测试双实现要容易得多。 此实现的代码如下所示:

  1. using System.Collections.Generic;
  2. using System.Data.Entity.Infrastructure;
  3. using System.Linq;
  4. using System.Linq.Expressions;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. namespace TestingDemo
  8. {
  9. internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
  10. {
  11. private readonly IQueryProvider _inner;
  12. internal TestDbAsyncQueryProvider(IQueryProvider inner)
  13. {
  14. _inner = inner;
  15. }
  16. public IQueryable CreateQuery(Expression expression)
  17. {
  18. return new TestDbAsyncEnumerable<TEntity>(expression);
  19. }
  20. public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
  21. {
  22. return new TestDbAsyncEnumerable<TElement>(expression);
  23. }
  24. public object Execute(Expression expression)
  25. {
  26. return _inner.Execute(expression);
  27. }
  28. public TResult Execute<TResult>(Expression expression)
  29. {
  30. return _inner.Execute<TResult>(expression);
  31. }
  32. public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
  33. {
  34. return Task.FromResult(Execute(expression));
  35. }
  36. public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
  37. {
  38. return Task.FromResult(Execute<TResult>(expression));
  39. }
  40. }
  41. internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
  42. {
  43. public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
  44. : base(enumerable)
  45. { }
  46. public TestDbAsyncEnumerable(Expression expression)
  47. : base(expression)
  48. { }
  49. public IDbAsyncEnumerator<T> GetAsyncEnumerator()
  50. {
  51. return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
  52. }
  53. IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
  54. {
  55. return GetAsyncEnumerator();
  56. }
  57. IQueryProvider IQueryable.Provider
  58. {
  59. get { return new TestDbAsyncQueryProvider<T>(this); }
  60. }
  61. }
  62. internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
  63. {
  64. private readonly IEnumerator<T> _inner;
  65. public TestDbAsyncEnumerator(IEnumerator<T> inner)
  66. {
  67. _inner = inner;
  68. }
  69. public void Dispose()
  70. {
  71. _inner.Dispose();
  72. }
  73. public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
  74. {
  75. return Task.FromResult(_inner.MoveNext());
  76. }
  77. public T Current
  78. {
  79. get { return _inner.Current; }
  80. }
  81. object IDbAsyncEnumerator.Current
  82. {
  83. get { return Current; }
  84. }
  85. }
  86. }

现在,我们有了一个异步查询提供程序,我们可以为新的 GetAllBlogsAsync 方法编写单元测试。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. using Moq;
  3. using System.Collections.Generic;
  4. using System.Data.Entity;
  5. using System.Data.Entity.Infrastructure;
  6. using System.Linq;
  7. using System.Threading.Tasks;
  8. namespace TestingDemo
  9. {
  10. [TestClass]
  11. public class AsyncQueryTests
  12. {
  13. [TestMethod]
  14. public async Task GetAllBlogsAsync_orders_by_name()
  15. {
  16. var data = new List<Blog>
  17. {
  18. new Blog { Name = "BBB" },
  19. new Blog { Name = "ZZZ" },
  20. new Blog { Name = "AAA" },
  21. }.AsQueryable();
  22. var mockSet = new Mock<DbSet<Blog>>();
  23. mockSet.As<IDbAsyncEnumerable<Blog>>()
  24. .Setup(m => m.GetAsyncEnumerator())
  25. .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));
  26. mockSet.As<IQueryable<Blog>>()
  27. .Setup(m => m.Provider)
  28. .Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider));
  29. mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  30. mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  31. mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
  32. var mockContext = new Mock<BloggingContext>();
  33. mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
  34. var service = new BlogService(mockContext.Object);
  35. var blogs = await service.GetAllBlogsAsync();
  36. Assert.AreEqual(3, blogs.Count);
  37. Assert.AreEqual("AAA", blogs[0].Name);
  38. Assert.AreEqual("BBB", blogs[1].Name);
  39. Assert.AreEqual("ZZZ", blogs[2].Name);
  40. }
  41. }
  42. }