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

时间:2012-03-25 13:38:29

标签: c# unit-testing

考虑以下单元测试示例。这些评论几乎解释了我的问题。

[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?

}

10 个答案:

答案 0 :(得分:26)

有两种方法可以做到这一点。 One正在测试类中的方法上使用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.
  }
}

如果要清理很多对象,那么try / finally清理会变得非常混乱。我的团队倾向于实现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.
}

这具有在方法调用中包装构造函数的额外好处。如果构造函数签名发生变化,我们可以轻松修改测试代码。

答案 1 :(得分:9)

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

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

因此,当您测试依赖于数据库数据的业务逻辑时,通常会使用类似Repository Pattern的内容并注入fake/stub/mock IXXXRepository进行单元测试。在测试具体的存储库时,您需要进行所询问的数据库清理,或者需要对基础数据库调用进行填充/存根。这完全取决于你。

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

在MS-Test中,用于声明设置/拆除的属性为ClassInitializeClassCleanUpTestInitializeTestCleanUp。其他框架也有类似命名的构造。

有许多框架可以帮助您进行模拟/存根:MoqRhino MocksNMockTypeMockMoles and Stubs(VS2010) ),VS11 Fakes(VS11 Beta)等。如果您正在寻找依赖注入框架,请查看NinjectUnityCastle Windsor等内容。

答案 2 :(得分:5)

一些回复:

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

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

答案 3 :(得分:1)

你的问题有点过于笼统。通常你应该在每次测试后清理。通常,您不能依赖所有测试始终以相同的顺序执行,并且您必须确定数据库中的内容。对于一般设置或清理,大多数单元测试框架提供了可以覆盖的setUp和tearDown方法,并将自动调用。我不知道它在C#中是如何工作的,但是e。 G。在JUnit(Java)中,您有这些方法。

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

答案 4 :(得分:0)

在这种情况下你必须做手动清理。即,与db中生成一些对象相反。

另一种方法是使用诸如Rhino Mocks之类的Mocking工具,这样数据库就只是一个内存数据库

答案 5 :(得分:0)

这取决于您实际测试的内容。看看评论,我会说,但顺便说一下,很难推断评论。在实践中,清理刚刚插入的对象,重置测试状态。因此,如果您进行清理,则开始从清理系统进行测试。

答案 6 :(得分:0)

我认为清理取决于你是如何构建数据的,所以如果“旧测试数据”不能与未来的测试运行相互作用,我认为把它留在后面是好的。

我在编写集成测试时一直采用的方法是让测试针对与应用程序db不同的数据库运行。我倾向于重建测试数据库作为每次测试运行的前提条件。这样,您就不需要为每个测试执行粒度清理方案,因为每次测试运行都会在运行之间获得干净的平台。 我使用SQL服务器完成了大部分开发工作,但在某些情况下,我在针对SQL Compact版本数据库运行测试时,可以快速有效地在运行之间重建。

答案 7 :(得分:0)

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

答案 8 :(得分:0)

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

希望有时这对某人有用。

04/02/17

无论断言是通过还是失败,finally块都会执行,它也不会引入错误传递的风险 - 这会导致捕获!

答案 9 :(得分:0)

以下是我使用的测试方法的框架。这使我可以使用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
        }
    }