在测试之间共享数据库Sharing databases between tests

EF Core 测试示例演示了如何针对不同的数据库系统测试应用程序。 对于该示例,每个测试都会创建一个新数据库。 当使用 SQLite 或 EF 内存中数据库时,这是一个很好的模式,但在使用其他数据库系统时,这种模式可能会造成很大的开销。

此示例通过将数据库创建移到测试装置中来构建前面的示例。 这允许创建单个 SQL Server 数据库,并只为所有测试创建一次。

提示

在继续操作之前,请务必完成EF Core 测试示例

为同一个数据库编写多个测试并不困难。 这一技巧以测试在运行时不会相互行程的方式进行操作。 这需要了解:

  • 如何在测试之间安全共享对象
  • 当测试框架并行运行测试时
  • 如何使数据库处于每个测试的干净状态

装置The fixture

我们将使用测试装置在测试之间共享对象。 当你想要创建单个测试上下文并将其共享到类中的所有测试中并在类中的所有测试都完成后, XUnit 文档将指出应使用的装置。 “

提示

此示例使用的是XUnit,但其他测试框架中也存在类似的概念,包括NUnit

这意味着我们需要将数据库创建和种子设定转移到夹具类。 如下所示:

  1. public class SharedDatabaseFixture : IDisposable
  2. {
  3. private static readonly object _lock = new object();
  4. private static bool _databaseInitialized;
  5. public SharedDatabaseFixture()
  6. {
  7. Connection = new SqlConnection(@"Server=(localdb)\mssqllocaldb;Database=EFTestSample;ConnectRetryCount=0");
  8. Seed();
  9. Connection.Open();
  10. }
  11. public DbConnection Connection { get; }
  12. public ItemsContext CreateContext(DbTransaction transaction = null)
  13. {
  14. var context = new ItemsContext(new DbContextOptionsBuilder<ItemsContext>().UseSqlServer(Connection).Options);
  15. if (transaction != null)
  16. {
  17. context.Database.UseTransaction(transaction);
  18. }
  19. return context;
  20. }
  21. private void Seed()
  22. {
  23. lock (_lock)
  24. {
  25. if (!_databaseInitialized)
  26. {
  27. using (var context = CreateContext())
  28. {
  29. context.Database.EnsureDeleted();
  30. context.Database.EnsureCreated();
  31. var one = new Item("ItemOne");
  32. one.AddTag("Tag11");
  33. one.AddTag("Tag12");
  34. one.AddTag("Tag13");
  35. var two = new Item("ItemTwo");
  36. var three = new Item("ItemThree");
  37. three.AddTag("Tag31");
  38. three.AddTag("Tag31");
  39. three.AddTag("Tag31");
  40. three.AddTag("Tag32");
  41. three.AddTag("Tag32");
  42. context.AddRange(one, two, three);
  43. context.SaveChanges();
  44. }
  45. _databaseInitialized = true;
  46. }
  47. }
  48. }
  49. public void Dispose() => Connection.Dispose();
  50. }

现在,请注意构造函数的方式:

  • 在装置的生存期内创建单个数据库连接
  • 通过调用Seed方法创建并设定该数据库的种子

立即忽略锁定;稍后我们将返回到它。

提示

创建和播种代码不需要是异步的。 将其设为异步会使代码变得复杂,而不会提高测试的性能或吞吐量。

数据库是通过先删除所有现有数据库,然后创建新的数据库创建的。 这可确保数据库与当前 EF 模型匹配,即使自上次测试运行以来已更改。

提示

使用respawn之类的内容(而不是每次重新创建),可以更快地 “清理” 现有数据库。 但是,在执行此操作时,必须注意确保数据库架构与 EF 模型保持最新。

释放装置后,数据库连接会被释放。 此时,您还可以考虑删除测试数据库。 但是,如果多个测试类正在共享该装置,则这将需要额外的锁定和引用计数。 此外,使测试数据库仍可用于调试失败的测试通常是很有用的。

使用装置Using the fixture

XUnit 具有一个通用模式,用于将测试装置与一类测试进行关联:

  1. public class SharedDatabaseTest : IClassFixture<SharedDatabaseFixture>
  2. {
  3. public SharedDatabaseTest(SharedDatabaseFixture fixture) => Fixture = fixture;
  4. public SharedDatabaseFixture Fixture { get; }

XUnit 现在将创建单个夹具实例,并将其传递给测试类的每个实例。 (请记住,XUnit 会在每次运行测试时创建新的测试类实例。)这意味着将创建数据库并将其设定为种子,然后每个测试都将使用此数据库。

请注意,不会并行运行单个类中的测试。 这意味着,即使DbConnection对象不是线程安全的,每个测试都可以安全地使用同一个数据库连接。

维护数据库状态Maintaining database state

测试通常需要对测试数据进行插入、更新和删除操作。 但这些更改会影响到需要干净的种子数据库的其他测试。

这可以通过在事务中运行转变测试来处理。 例如:

  1. [Fact]
  2. public void Can_add_item()
  3. {
  4. using (var transaction = Fixture.Connection.BeginTransaction())
  5. {
  6. using (var context = Fixture.CreateContext(transaction))
  7. {
  8. var controller = new ItemsController(context);
  9. var item = controller.PostItem("ItemFour").Value;
  10. Assert.Equal("ItemFour", item.Name);
  11. }
  12. using (var context = Fixture.CreateContext(transaction))
  13. {
  14. var item = context.Set<Item>().Single(e => e.Name == "ItemFour");
  15. Assert.Equal("ItemFour", item.Name);
  16. Assert.Equal(0, item.Tags.Count);
  17. }
  18. }
  19. }

请注意,在测试完成后将创建事务,并将其释放。 释放事务将导致回滚该事务,因此其他测试将看不到任何更改。

用于创建上下文的帮助器方法(请参阅上面的装置代码)接受此事务,并使用它来 DbContext。

共享装置Sharing the fixture

您可能已注意到锁定了有关数据库创建和播种的代码。 这不是此示例所必需的,因为只有一类测试使用装置,因此仅创建单个装置实例。

但是,你可能想要将相同的装置用于多个测试类别。 XUnit 将为这些类中的每个类创建一个装置实例。 并行运行测试的不同线程可能会使用这些方法。 因此,请务必使用适当的锁定,以确保只有一个线程可以创建和播种数据库。

提示

这里简单lock明了。 无需尝试更复杂的操作,如任何无锁模式。