使用 SQLite 测试

SQLite 有一个内存模式,它允许你使用 SQLite 来编写针对关系数据库的测试,并且不会造成实际数据库的操作开销。

提示

你可以在 GitHub 上查阅当前文章涉及的代码样例

样例测试场景

考虑以下服务,其允许应用程序代码执行一些 blog 相关的操作。其内部使用的是链接到 SQL Server 数据库的 DbContext。将上下文切换链接到内存 SQLite 数据库将会很有用,这样的话我们无需修改源代码或者做大量工作来重复为上下文创建测试,就可以对该服务编写高效的测试代码。

  1. using System.Collections.Generic;
  2. using System.Linq;
  3. namespace BusinessLogic
  4. {
  5. public class BlogService
  6. {
  7. private BloggingContext _context;
  8. public BlogService(BloggingContext context)
  9. {
  10. _context = context;
  11. }
  12. public void Add(string url)
  13. {
  14. var blog = new Blog { Url = url };
  15. _context.Blogs.Add(blog);
  16. _context.SaveChanges();
  17. }
  18. public IEnumerable<Blog> Find(string term)
  19. {
  20. return _context.Blogs
  21. .Where(b => b.Url.Contains(term))
  22. .OrderBy(b => b.Url)
  23. .ToList();
  24. }
  25. }
  26. }

准备上下文

避免配置多个提供程序

在测试中你将在外部配置 context 为使用内存提供程序。如果你通过重写 context 的 OnConfiguring 来配置数据库提供程序,那么你就要添加一些条件代码才能确保在没有配置提供程序时才配置它。

提示

如果你正在使用 ASP.NET Core,那么你就不需要这些代码,因为数据库提供程序是在 context 之外被配置的(在 Startup.cs 中)。

  1. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  2. {
  3. if (!optionsBuilder.IsConfigured)
  4. {
  5. optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
  6. }
  7. }

为测试添加构造方法

针对不同的数据库,启用测试的最简单方法是修改上下文类型以暴露一个接受 DbContextOptions<TContext> 参数的构造方法。

  1. public class BloggingContext : DbContext
  2. {
  3. public BloggingContext()
  4. { }
  5. public BloggingContext(DbContextOptions<BloggingContext> options)
  6. : base(options)
  7. { }

提示

DbContextOptions<TContext> 用于传递上下文配置信息,比如链接到哪个数据库。这与运行 context 的 OnConfiguring 方法所构建的是同一个对象。

编写测试

使用该提供程序进行测试的关键点是告知上下文要使用 SQLite 的能力,以及控制内存数据库范围的能力。数据库范围是通过打开和关系链接来控制的。数据库会被限定为链接打开的期间。通常你会想要为每个测试方法都清理数据库。

  1. using BusinessLogic;
  2. using Microsoft.Data.Sqlite;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.VisualStudio.TestTools.UnitTesting;
  5. using System.Linq;
  6. namespace TestProject.SQLite
  7. {
  8. [TestClass]
  9. public class BlogServiceTests
  10. {
  11. [TestMethod]
  12. public void Add_writes_to_database()
  13. {
  14. // 内存数据库只在链接打开时存在
  15. var connection = new SqliteConnection("DataSource=:memory:");
  16. connection.Open();
  17. try
  18. {
  19. var options = new DbContextOptionsBuilder<BloggingContext>()
  20. .UseSqlite(connection)
  21. .Options;
  22. // 在数据库中创建模式
  23. using (var context = new BloggingContext(options))
  24. {
  25. context.Database.EnsureCreated();
  26. }
  27. // 针对一个 context 实例运行测试
  28. using (var context = new BloggingContext(options))
  29. {
  30. var service = new BlogService(context);
  31. service.Add("http://sample.com");
  32. }
  33. // 使用独立的 context 实例验证是否已将正确的数据保存到了数据库
  34. using (var context = new BloggingContext(options))
  35. {
  36. Assert.AreEqual(1, context.Blogs.Count());
  37. Assert.AreEqual("http://sample.com", context.Blogs.Single().Url);
  38. }
  39. }
  40. finally
  41. {
  42. connection.Close();
  43. }
  44. }
  45. [TestMethod]
  46. public void Find_searches_url()
  47. {
  48. // 内存数据库只在链接打开时存在
  49. var connection = new SqliteConnection("DataSource=:memory:");
  50. connection.Open();
  51. try
  52. {
  53. var options = new DbContextOptionsBuilder<BloggingContext>()
  54. .UseSqlite(connection)
  55. .Options;
  56. // 在数据库中创建模式
  57. using (var context = new BloggingContext(options))
  58. {
  59. context.Database.EnsureCreated();
  60. }
  61. // 使用一个 context 实例将种子数据插入到数据库中
  62. using (var context = new BloggingContext(options))
  63. {
  64. context.Blogs.Add(new Blog { Url = "http://sample.com/cats" });
  65. context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
  66. context.Blogs.Add(new Blog { Url = "http://sample.com/dogs" });
  67. context.SaveChanges();
  68. }
  69. // 用于清理运行测试的 context 实例
  70. using (var context = new BloggingContext(options))
  71. {
  72. var service = new BlogService(context);
  73. var result = service.Find("cat");
  74. Assert.AreEqual(2, result.Count());
  75. }
  76. }
  77. finally
  78. {
  79. connection.Close();
  80. }
  81. }
  82. }
  83. }