用自己的测试进行测试双精度Testing with your own test doubles

备注

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

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

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

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

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

本文将介绍如何创建自己的测试 double。 有关使用模拟框架的信息,请参阅使用模拟框架进行测试

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

本文中所示的代码与 EF6 兼容。 若要使用 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 的部分。

创建上下文接口Creating a context interface

我们将介绍如何测试使用 EF 模型的服务。 为了能够将 EF 上下文替换为内存中用于测试的版本,我们将定义 EF 上下文(和内存中双精度)将实现的接口。

我们将要测试的服务将使用上下文的 DbSet 属性来查询和修改数据,还会调用 SaveChanges 将更改推送到数据库。 我们将这些成员包括在接口上。

  1. using System.Data.Entity;
  2. namespace TestingDemo
  3. {
  4. public interface IBloggingContext
  5. {
  6. DbSet<Blog> Blogs { get; }
  7. DbSet<Post> Posts { get; }
  8. int SaveChanges();
  9. }
  10. }

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, IBloggingContext
  6. {
  7. public DbSet<Blog> Blogs { get; set; }
  8. public 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. }

通过 EF 设计器实现上下文接口Implementing the context interface with the EF Designer

请注意,我们的上下文实现了 IBloggingContext 接口。

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

  1. <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IBloggingContext

要测试的服务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 IBloggingContext _context;
  10. public BlogService(IBloggingContext context)
  11. {
  12. _context = context;
  13. }
  14. public Blog AddBlog(string name, string url)
  15. {
  16. var blog = new Blog { Name = name, Url = url };
  17. _context.Blogs.Add(blog);
  18. _context.SaveChanges();
  19. return blog;
  20. }
  21. public List<Blog> GetAllBlogs()
  22. {
  23. var query = from b in _context.Blogs
  24. orderby b.Name
  25. select b;
  26. return query.ToList();
  27. }
  28. public async Task<List<Blog>> GetAllBlogsAsync()
  29. {
  30. var query = from b in _context.Blogs
  31. orderby b.Name
  32. select b;
  33. return await query.ToListAsync();
  34. }
  35. }
  36. }

创建内存中测试双精度Creating the in-memory test doubles

现在,我们已经有了真正的 EF 模型和可使用该模型的服务,接下来可以创建可用于测试的内存中测试双精度。 我们为上下文创建了 TestContext 测试 double。 在测试中,我们会选择所需的行为,以便支持我们将要运行的测试。 在此示例中,我们只是捕获调用 SaveChanges 的次数,但你可以包括验证所测试方案所需的任何逻辑。

我们还创建了 TestDbSet 来提供 DbSet 的内存中实现。 我们为 DbSet 上的所有方法提供了一个完整的实现(Find 除外),但你只需实现测试方案将使用的成员。

TestDbSet 利用我们提供的其他一些基础结构类,以确保可以处理异步查询。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.Data.Entity;
  5. using System.Data.Entity.Infrastructure;
  6. using System.Linq;
  7. using System.Linq.Expressions;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. namespace TestingDemo
  11. {
  12. public class TestContext : IBloggingContext
  13. {
  14. public TestContext()
  15. {
  16. this.Blogs = new TestDbSet<Blog>();
  17. this.Posts = new TestDbSet<Post>();
  18. }
  19. public DbSet<Blog> Blogs { get; set; }
  20. public DbSet<Post> Posts { get; set; }
  21. public int SaveChangesCount { get; private set; }
  22. public int SaveChanges()
  23. {
  24. this.SaveChangesCount++;
  25. return 1;
  26. }
  27. }
  28. public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
  29. where TEntity : class
  30. {
  31. ObservableCollection<TEntity> _data;
  32. IQueryable _query;
  33. public TestDbSet()
  34. {
  35. _data = new ObservableCollection<TEntity>();
  36. _query = _data.AsQueryable();
  37. }
  38. public override TEntity Add(TEntity item)
  39. {
  40. _data.Add(item);
  41. return item;
  42. }
  43. public override TEntity Remove(TEntity item)
  44. {
  45. _data.Remove(item);
  46. return item;
  47. }
  48. public override TEntity Attach(TEntity item)
  49. {
  50. _data.Add(item);
  51. return item;
  52. }
  53. public override TEntity Create()
  54. {
  55. return Activator.CreateInstance<TEntity>();
  56. }
  57. public override TDerivedEntity Create<TDerivedEntity>()
  58. {
  59. return Activator.CreateInstance<TDerivedEntity>();
  60. }
  61. public override ObservableCollection<TEntity> Local
  62. {
  63. get { return _data; }
  64. }
  65. Type IQueryable.ElementType
  66. {
  67. get { return _query.ElementType; }
  68. }
  69. Expression IQueryable.Expression
  70. {
  71. get { return _query.Expression; }
  72. }
  73. IQueryProvider IQueryable.Provider
  74. {
  75. get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
  76. }
  77. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  78. {
  79. return _data.GetEnumerator();
  80. }
  81. IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
  82. {
  83. return _data.GetEnumerator();
  84. }
  85. IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
  86. {
  87. return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
  88. }
  89. }
  90. internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
  91. {
  92. private readonly IQueryProvider _inner;
  93. internal TestDbAsyncQueryProvider(IQueryProvider inner)
  94. {
  95. _inner = inner;
  96. }
  97. public IQueryable CreateQuery(Expression expression)
  98. {
  99. return new TestDbAsyncEnumerable<TEntity>(expression);
  100. }
  101. public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
  102. {
  103. return new TestDbAsyncEnumerable<TElement>(expression);
  104. }
  105. public object Execute(Expression expression)
  106. {
  107. return _inner.Execute(expression);
  108. }
  109. public TResult Execute<TResult>(Expression expression)
  110. {
  111. return _inner.Execute<TResult>(expression);
  112. }
  113. public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
  114. {
  115. return Task.FromResult(Execute(expression));
  116. }
  117. public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
  118. {
  119. return Task.FromResult(Execute<TResult>(expression));
  120. }
  121. }
  122. internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
  123. {
  124. public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
  125. : base(enumerable)
  126. { }
  127. public TestDbAsyncEnumerable(Expression expression)
  128. : base(expression)
  129. { }
  130. public IDbAsyncEnumerator<T> GetAsyncEnumerator()
  131. {
  132. return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
  133. }
  134. IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
  135. {
  136. return GetAsyncEnumerator();
  137. }
  138. IQueryProvider IQueryable.Provider
  139. {
  140. get { return new TestDbAsyncQueryProvider<T>(this); }
  141. }
  142. }
  143. internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
  144. {
  145. private readonly IEnumerator<T> _inner;
  146. public TestDbAsyncEnumerator(IEnumerator<T> inner)
  147. {
  148. _inner = inner;
  149. }
  150. public void Dispose()
  151. {
  152. _inner.Dispose();
  153. }
  154. public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
  155. {
  156. return Task.FromResult(_inner.MoveNext());
  157. }
  158. public T Current
  159. {
  160. get { return _inner.Current; }
  161. }
  162. object IDbAsyncEnumerator.Current
  163. {
  164. get { return Current; }
  165. }
  166. }
  167. }

实现查找Implementing Find

Find 方法难以以一般方式实现。 如果需要测试使用 Find 方法的代码,最简单的方法是为需要支持 find 的每个实体类型创建测试 DbSet。 然后,可以编写逻辑来查找特定类型的实体,如下所示。

  1. using System.Linq;
  2. namespace TestingDemo
  3. {
  4. class TestBlogDbSet : TestDbSet<Blog>
  5. {
  6. public override Blog Find(params object[] keyValues)
  7. {
  8. var id = (int)keyValues.Single();
  9. return this.SingleOrDefault(b => b.BlogId == id);
  10. }
  11. }
  12. }

编写一些测试Writing some tests

这就是开始测试时需要执行的所有操作。 以下测试将创建 TestContext,然后基于此上下文创建一个服务。 然后,使用 AddBlog 方法创建新的博客。 最后,此测试将验证服务是否已将新的博客添加到上下文的 “博客” 属性,并在上下文中调用 “SaveChanges”。

这只是您可以使用内存中测试双精度测试的类型的示例,您可以根据您的要求调整测试的逻辑加倍和验证。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. using System.Linq;
  3. namespace TestingDemo
  4. {
  5. [TestClass]
  6. public class NonQueryTests
  7. {
  8. [TestMethod]
  9. public void CreateBlog_saves_a_blog_via_context()
  10. {
  11. var context = new TestContext();
  12. var service = new BlogService(context);
  13. service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");
  14. Assert.AreEqual(1, context.Blogs.Count());
  15. Assert.AreEqual("ADO.NET Blog", context.Blogs.Single().Name);
  16. Assert.AreEqual("http://blogs.msdn.com/adonet", context.Blogs.Single().Url);
  17. Assert.AreEqual(1, context.SaveChangesCount);
  18. }
  19. }
  20. }

下面是测试的另一个示例-这是执行查询的另一个示例。 首先,通过在其 “博客” 属性中创建包含某些数据的测试上下文来开始测试,请注意,数据不按字母顺序排序。 然后,可以基于测试上下文创建 BlogService,并确保从 GetAllBlogs 返回的数据按名称排序。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. namespace TestingDemo
  3. {
  4. [TestClass]
  5. public class QueryTests
  6. {
  7. [TestMethod]
  8. public void GetAllBlogs_orders_by_name()
  9. {
  10. var context = new TestContext();
  11. context.Blogs.Add(new Blog { Name = "BBB" });
  12. context.Blogs.Add(new Blog { Name = "ZZZ" });
  13. context.Blogs.Add(new Blog { Name = "AAA" });
  14. var service = new BlogService(context);
  15. var blogs = service.GetAllBlogs();
  16. Assert.AreEqual(3, blogs.Count);
  17. Assert.AreEqual("AAA", blogs[0].Name);
  18. Assert.AreEqual("BBB", blogs[1].Name);
  19. Assert.AreEqual("ZZZ", blogs[2].Name);
  20. }
  21. }
  22. }

最后,我们将编写一个使用异步方法的测试,以确保TestDbSet中包含的异步基础结构工作正常。

  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. namespace TestingDemo
  6. {
  7. [TestClass]
  8. public class AsyncQueryTests
  9. {
  10. [TestMethod]
  11. public async Task GetAllBlogsAsync_orders_by_name()
  12. {
  13. var context = new TestContext();
  14. context.Blogs.Add(new Blog { Name = "BBB" });
  15. context.Blogs.Add(new Blog { Name = "ZZZ" });
  16. context.Blogs.Add(new Blog { Name = "AAA" });
  17. var service = new BlogService(context);
  18. var blogs = await service.GetAllBlogsAsync();
  19. Assert.AreEqual(3, blogs.Count);
  20. Assert.AreEqual("AAA", blogs[0].Name);
  21. Assert.AreEqual("BBB", blogs[1].Name);
  22. Assert.AreEqual("ZZZ", blogs[2].Name);
  23. }
  24. }
  25. }