NUnit - 测试失败后的清理工作

43
我们有一些访问数据库的 NUnit 测试。当其中一个测试失败时,它可能会使数据库处于不一致的状态 - 这不是问题,因为我们每次测试运行都会重建数据库 - 但它可能导致同一次运行中的其他测试也失败。
是否可以检测到某个测试失败并执行某种清理操作?
我们不想在每个测试中编写清理代码,我们已经这样做了。我想在 Teardown 中执行清理,但仅在测试失败时执行,因为清理可能很昂贵。
更新:澄清一下 - 我希望测试简单,并且不包含任何清理或错误处理逻辑。我也不想在每次测试运行时进行数据库重置 - 只有测试失败时才需要。而且,这段代码应该在 Teardown 方法中执行,但我不知道有没有办法获取当前正在撤销的测试是否失败或成功。
更新2:
        [Test]
        public void MyFailTest()
        {
            throw new InvalidOperationException();
        }

        [Test]
        public void MySuccessTest()
        {
            Assert.That(true, Is.True);
        }

        [TearDown]
        public void CleanUpOnError()
        {
            if (HasLastTestFailed()) CleanUpDatabase();
        }

我正在寻找 HasLastTestFailed() 的实现。


如果你不想在每个测试或每次测试之后清理,那么就不会发生任何清理。抱歉。 - rein
11个回答

75
自版本2.5.7起,NUnit允许Teardown检测上一个测试是否失败。新的TestContext类允许测试访问关于自身的信息,包括TestStauts。
更多详情请参考http://nunit.org/?p=releaseNotes&r=2.5.7
[TearDown]
public void TearDown()
{
    if (TestContext.CurrentContext.Result.Status == TestStatus.Failed)
    {
        PerformCleanUpFromTest();
    }
}

太好了,我不知道有TestContext类。最新版本的NUnit将Status枚举直接移动到TestContext类上,所以应该是:TestContext.Status == TestStatus.Failed。 - Darrell Mozingo
使用这种方法,是否还有一种获取失败原因的方式? 例如:导致失败的异常。 - Robot Mess
4
在NUnit 3.0中,TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed表示测试失败。 - mattbloke

21

这个想法让我很感兴趣,所以我进行了一些探索。NUnit并没有原生支持这种功能,但是它提供了一个完整的可扩展性框架。我找到了这篇关于扩展NUnit的好文章 - 它是一个很好的起点。在尝试过后,我得到了以下解决方案:用自定义CleanupOnError属性装饰的方法将在测试套件中的任何一个测试失败时被调用。

下面是测试的样子:

  [TestFixture]
  public class NUnitAddinTest
  {
    [CleanupOnError]
    public static void CleanupOnError()
    {
      Console.WriteLine("There was an error, cleaning up...");
      // perform cleanup logic
    }

    [Test]
    public void Test1_this_test_passes()
    {
      Console.WriteLine("Hello from Test1");
    }

    [Test]
    public void Test2_this_test_fails()
    {
      throw new Exception("Test2 failed");
    }

    [Test]
    public void Test3_this_test_passes()
    {
      Console.WriteLine("Hello from Test3");
    }
  }

属性只需简单地为:

  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
  public sealed class CleanupOnErrorAttribute : Attribute
  {
  }

这是插件中如何执行它的方法:

public void RunFinished(TestResult result)
{
  if (result.IsFailure)
  {
    if (_CurrentFixture != null)
    {
      MethodInfo[] methods = Reflect.GetMethodsWithAttribute(_CurrentFixture.FixtureType,
                                                             CleanupAttributeFullName, false);
      if (methods == null || methods.Length == 0)
      {
        return;
      }

      Reflect.InvokeMethod(methods[0], _CurrentFixture);
    }
  }
}

但是这里有个棘手的问题:插件必须放在与NUnit运行器相邻的addins目录中。我把它放在了TestDriven.NET目录下的NUnit运行器旁边:

C:\Program Files\TestDriven.NET 2.0\NUnit\addins

(我创建了addins目录,它原本不存在)

编辑另一个问题是清除方法需要是static的!

我拼凑出了一个简单的插件,你可以从我的SkyDrive下载源代码。你需要在适当的地方添加nunit.framework.dllnunit.core.dllnunit.core.interfaces.dll三个引用。

几个注意事项:属性类可以放在代码中的任何位置。我不想将它放在与插件本身相同的程序集中,因为它引用了两个Core NUnit程序集,所以我将它放在了另一个程序集中。如果你决定将它放在其他地方,请记得更改CleanAddin.cs中的代码行。

希望这可以帮到你。


你的实现只有集成测试。我创建了它的一个变体,遇到了各种问题。能否回答我的问题,如何为NUnit插件编写单元测试?http://stackoverflow.com/questions/3927349/how-to-write-a-nunit-test-for-an-nunit-add-in - Precipitous

2
是的,可以使用Teardown属性,在每个测试后进行拆卸。您需要应用您拥有的数据库“重置”脚本,并在每个测试之前和之后进行拆卸和重新设置。

此属性用于在 TestFixture 中提供一组常用函数,这些函数在运行每个测试方法后执行。

更新:根据评论和问题的更新,我认为您可以使用teardown属性并使用私有变量来指示是否应触发方法内容。
虽然,我也看到您不想要任何复杂的逻辑或错误处理代码。
鉴于这一点,我认为标准的Setup/Teardown最适合您。如果出现错误,也没有关系,您不必编写任何错误处理代码。
如果您需要进行特殊的清理,因为下一个测试依赖于当前测试的成功完成,我建议重新审视您的测试--它们可能不应该相互依赖。

我认为他知道Teardown,他想做的是在Teardown时意识到夹具中是否有任何测试失败,如果没有,他可以跳过数据库重建。 - Ray Hayes
嗯,生成与测试无关的测试数据是可能的,但为每个测试设置数据库会花费更长的时间,而编写清理代码则相对较快。我的目标是减少构建时间并简化设置/拆卸代码。不过,对于这些测试来说,模拟是不可行的选择,因为我们已经在单元测试中使用了它们。 - bh213
请查看我的抽象类解决方案,以扩展这个想法。 - Dan McClain
从nunit文档中:如果TestFixtureSetUp方法失败或抛出异常,则不会运行TestFixtureTearDown。 - Precipitous

2

虽然可能可以强制nUnit这样做,但这并不是最明智的设计,您可以在某处设置一个临时文件,如果该文件存在,则运行清理操作。

我建议更改您的代码,使数据库事务启用,并在测试结束时将数据库恢复到原始状态(例如,放弃表示单元测试的事务)。


可能会有多个事务,我们还有使用REST/WCF的测试,其中不存在事务。我们谈论的是自动化测试,而不是单元测试。 - bh213
1
nUnit是正确的选择吗?如果你不想要单元测试框架的默认行为,而且你也说这些不是单元测试...听起来nUnit可能不是合适的工具!为什么不只是编写一些控制台应用程序,并让批处理文件或msbuild文件按顺序调用它们呢? - Ray Hayes
@Ray- 我也是这么想的 +1。一开始我没看到你的回答。我的回答详细解释了如何使用TransactionScope来实现此操作。 - RichardOD

1

使用Try-Catch块,重新抛出捕获的异常怎么样?

try
{
//Some assertion
}
catch
{
     CleanUpMethod();
     throw;
}

那样做不是每个测试都需要详细解释吗?“我们不想在每个测试中编写清理代码” - Ray Hayes
是的,我已经发布了第二个解决方案以更好地应对更新的评论。 - Dan McClain

1
另一种选择是拥有一个特殊函数,它将抛出您的异常,并在测试夹具中设置一个开关,指示发生了异常。
public abstract class CleanOnErrorFixture
{
     protected bool threwException = false;

     protected void ThrowException(Exception someException)
     {
         threwException = true;
         throw someException;
     }

     protected bool HasTestFailed()
     {
          if(threwException)
          {
               threwException = false; //So that this is reset after each teardown
               return true;
          }
          return false;
     }
}

然后使用您的示例:

[TestFixture]
public class SomeFixture : CleanOnErrorFixture
{
    [Test]
    public void MyFailTest()
    {
        ThrowException(new InvalidOperationException());
    }

    [Test]
    public void MySuccessTest()
    {
        Assert.That(true, Is.True);
    }

    [TearDown]
    public void CleanUpOnError()
    {
        if (HasLastTestFailed()) CleanUpDatabase();
    }
}

唯一的问题在于堆栈跟踪将导致CleanOnErrorFixture


+1 但这仍然需要错误处理,而bh213试图避免这个问题。这是一个不错的解决方案,但与所要求的不完全匹配...这可能是迄今为止给出的大多数答案的情况... - Frank V
在他的例子中,他有一个“坏测试”抛出异常。抽象类提供了与示例相同的功能,同时实现了他所需的“HasLastTestFailed”方法。从这个类继承不会像try catch块一样添加错误处理。这个例子只需要改变如何抛出异常(使用方法而不是throw)。 - Dan McClain
-1 要求系统在测试代码中调用方法(CleanOnErrorFixture.ThrowException)。 - Frank Schwieterman

1
我建议你现在先像phsr建议的那样做,等你有能力时再重构测试,使它们永远不必依赖于另一个测试需要的相同数据,或者更好地抽象数据访问层并模拟来自该数据库的结果。听起来你的测试相当昂贵,你应该在数据库中执行所有查询逻辑,在你的程序集中执行业务逻辑,你并不真正关心返回的结果是什么。
这样你也能更好地测试你的异常处理。

1

到目前为止没有提到的一个选项是将测试封装在TransactionScope对象中,这样无论发生什么情况,测试都不会提交任何内容到数据库。

这里有一些关于该技术的详细信息。如果你搜索单元测试和TransactionScope,可能会找到更多相关信息(尽管如果你访问了数据库,你实际上正在进行集成测试)。我过去成功地使用过它。

这种方法简单易行,不需要任何清理工作,并确保测试是隔离的。

编辑- 我刚刚注意到Ray Hayes的答案也与我的类似。


1
你可以添加一个[TearDown]方法,其中包含if (TestContext.CurrentContext.Result.Status != TestStatus.Passed)的代码,如果测试失败,将执行一些代码。

0

我并不是说这是个好主意,但它应该能够运行。 记住,断言失败就是异常。另外,不要忘记还有一个 [TestFixtureTearDown] 属性,它在测试套件中的所有测试运行完毕后仅运行一次。

利用这两个事实,您可以编写一些代码,例如设置一个标志来表示测试失败,并在测试套件撤销时检查该标志的值。

我不建议这样做,但它确实会起作用。您并没有真正按照 NUnit 的预期使用它,但您可以这么做。


[TestFixture]
public class Tests {
     private bool testsFailed = false;

     [Test]
     public void ATest() {
         try {
             DoSomething();
             Assert.AreEqual(....);
         } catch {
            testFailed = true;
         }
     }

     [TestFixtureTearDown]
     public void CleanUp() {
          if (testsFailed) {
              DoCleanup();
          }
     }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接