如何确保数据库清理始终在测试后执行

本文关键字:测试 执行 何确保 确保 数据库 | 更新日期: 2023-09-27 18:28:52

考虑下面的单元测试示例。这些评论很好地解释了我的问题。

[TestMethod]
public void MyTestMethod()
{
  //generate some objects in the database
  ...
  //make an assert that fails sometimes (for example purposes, this fails always)
  Assert.IsTrue(false);
  //TODO: how do we clean up the data generated in the database now that the test has ended here?
}

如何确保数据库清理始终在测试后执行

有两种方法可以做到这一点。一种是在测试类中的方法上使用TestInitialize和TestCleanup属性。它们将始终分别在测试之前和之后运行。

另一种方法是使用测试失败通过异常传播到测试运行程序的事实。这意味着在断言失败后,测试中的try{}finally{}块可以用来清理任何内容。

[TestMethod]
public void FooTest()
{
  try
  {
     // setup some database objects
     Foo foo = new Foo();
     Bar bar = new Bar(foo);
     Assert.Fail();
  }
  finally
  {
     // remove database objects.
  }
}

尝试/最终清理可能会变得非常混乱,因为有很多对象需要清理。我的团队倾向于实现IDisposable的助手类。它跟踪已创建的对象,并将它们推送到堆栈中。当调用Dispose时,这些项会从堆栈中弹出并从数据库中删除。

[TestMethod]
public void FooTest()
{
  using (FooBarDatabaseContext context = new FooBarDatabaseContext())
  {
    // setup some db objects.
    Foo foo = context.NewFoo();
    Bar bar = context.NewBar(foo);
    Assert.Fail();
  } // calls dispose. deletes bar, then foo.
}

这还有一个额外的好处,那就是在方法调用中包装构造函数。如果构造函数签名发生变化,我们可以很容易地修改测试代码。

我认为在这种情况下,最好的答案是仔细考虑您要测试的内容。理想情况下,单元测试应该尝试测试关于单个方法或函数的单个事实。当你开始将许多东西组合在一起时,它就会进入集成测试的世界(它们同样有价值,但不同)。

出于单元测试的目的,为了使您能够只测试想要测试的东西,您需要设计可测试性。这通常涉及到接口的额外使用(我从您展示的代码中假设是.NET)和某种形式的依赖注入(但不需要IoC/DI容器,除非您想要)。它还受益于并鼓励您在系统中创建非常有凝聚力(单一目的)和解耦(软依赖)的类。

因此,当您测试依赖于数据库数据的业务逻辑时,您通常会使用类似Repository模式的东西,并在中注入一个fake/stub/mock-IXXRepository进行单元测试。当您测试具体的存储库时,您要么需要执行您询问的那种数据库清理,要么需要填充/存根底层数据库调用。这真的取决于你。

当您确实需要创建/填充/清理数据库时,您可能会考虑利用大多数测试框架中可用的各种设置和拆卸方法。但是要小心,因为其中一些测试是在每次测试前后运行的,这会严重影响单元测试的性能。运行速度太慢的测试不会经常运行,这很糟糕。

在MS Test中,用于声明设置/拆卸的属性是ClassInitialize、ClassCleanUp、TestInitialize、TestCleanUp。其他框架也有类似名称的构造。

有许多框架可以帮助您进行嘲讽/存根:Moq、Rhino Mocks、NMock、TypeMock、Moles and Stubs(VS2010)、VS11 Fakes(VS11 Beta)等。如果您正在寻找依赖注入框架,请查看Ninject、Unity、Castle Windsor等。

  1. 如果它使用的是一个实际的数据库,我认为它不是严格意义上的"单元测试"。这是一个集成测试。单元测试不应该有这样的副作用。考虑使用一个模拟库来模拟实际的数据库。Rhino Mocks是其中之一,但还有很多其他的。

  2. 然而,如果这个测试的整个实际上是与数据库交互,那么您将希望与仅瞬态测试的数据库交互。在这种情况下,自动化测试的一部分将包括从头开始构建测试数据库,然后运行测试,然后销毁测试数据库的代码。同样,这个想法是没有外部副作用。可能有多种方法可以实现这一点,而且我对单元测试框架还不够熟悉,无法真正给出具体的建议。但是,如果您使用的是Visual Studio内置的测试,那么Visual Studio数据库项目可能会很有用。

您的问题有点过于笼统。通常你应该在每次测试后清理干净。通常情况下,你不能相信所有的测试总是以相同的顺序执行,你必须确定数据库中有什么。对于一般的设置或清理,大多数单元测试框架都提供了setup和tearDown方法,您可以重写这些方法,并将自动调用它们。我不知道这在C#中是如何工作的,但例如在JUnit(Java)中,你有这些方法。

我同意大卫的观点。你的测试通常应该没有副作用。您应该为每个测试设置一个新的数据库。

在这种情况下,您将不得不进行手动清理。也就是说,与的相反,在数据库中生成一些对象。

另一种选择是使用Rhino Mocks等Mocking工具,这样数据库就只是一个内存中的数据库

这取决于您实际测试的内容。看着评论,我会说,但顺便说一句,看着评论很难推断。清理刚刚插入的对象,在实践中,可以重置测试的状态。所以若你们清理,你们就从清理系统开始测试。

我认为清理取决于如何构建数据,所以如果"旧测试数据"与未来的测试运行不交互,我认为可以将其抛在后面。

在编写集成测试时,我一直采用的一种方法是让测试在不同于应用程序数据库的数据库中运行。我倾向于将重新构建测试数据库作为每次测试运行的先决条件。这样,您就不需要为每个测试提供细粒度的清理方案,因为每个测试运行在运行之间都会得到一个干净的记录。我的大部分开发都是使用SQL server完成的,但在某些情况下,我会针对SQL Compact edition数据库运行测试,这可以在运行之间快速高效地重建。

mbUnit有一个非常方便的属性Rollback,可以在完成测试后清理数据库。但是,您必须配置DTC(分布式事务协调器)才能使用它。

我遇到了一个类似的问题,一个测试的断言阻止了清理并导致其他测试失败。

希望这对某些人有用。

    [Test]
    public void Collates_Blah_As_Blah()
    {
        Assert.False(SINGLETON.Collection.Any());
        for (int i = 0; i < 2; i++)
            Assert.That(PROCESS(ValidRequest) == Status.Success);
        try
        {
            Assert.AreEqual(1, SINGLETON.Collection.Count);
        }
        finally
        {
            SINGLETON.Collection.Clear();
        }
    }

无论断言通过还是失败,finally块都将执行,它也不会引入错误通过的风险——这将导致catch!

以下是我使用的测试方法的框架。这使我可以使用try-catch finally在finally块中执行清理代码,而不会丢失失败的断言。

    [TestMethod]
    public void TestMethod1()
    {
        Exception defaultException = new Exception("No real execption.");
        try
        {
            #region Setup
            #endregion
            #region Tests
            #endregion
        }
        catch (Exception exc)
        { 
            /*if an Assert fails this catches its Exception so that it can be thrown 
            in the finally block*/
            defaultException = exc; 
        }
        finally
        {
            #region Cleanup
            //cleanup code goes here 
            if (!defaultException.Message.Equals("No real execption."))
            {
                throw defaultException;
            }
            #endregion
        }
    }