setUp/tearDown (@Before/@After) 在 JUnit 中的作用是什么?

71
我相信我们都知道 setUp (@Before) 方法在测试方法之前执行,而 tearDown(@After) 方法则在测试方法执行后执行。
此外,我们也知道Junit会为每个测试方法创建一个Test实例。
我的问题是,我们能否将 setUp 方法的内容移到类构造函数中并删除 setUp 方法?保留setUp方法的具体原因是否存在?
答案:可以将 setUp 方法的内容移动到类的构造函数中,并删除 setUp 方法。 但是需要注意的是,如果这样做,每个测试方法都将共享同一个测试实例,并且可能会影响测试结果的准确性。因此,如果测试方法之间有依赖关系或者需要重置状态,则必须使用 setUp 方法来确保测试的独立性和可重复性。
6个回答

61

这篇(旧的)JUnit最佳实践文章是这样说的:

不要使用测试用例构造函数来设置测试用例

在构造函数中设置测试用例不是一个好主意。请考虑:

public class SomeTest extends TestCase
   public SomeTest (String testName) {
      super (testName);
      // Perform test set-up
   }
}

假设在执行设置时,设置代码抛出一个 IllegalStateException 异常。作为响应,JUnit 会抛出一个 AssertionFailedError 异常,表示无法实例化测试用例。以下是 resulting stack trace 的示例:

junit.framework.AssertionFailedError: Cannot instantiate test case: test1   
    at junit.framework.Assert.fail(Assert.java:143)
    at junit.framework.TestSuite.runTest(TestSuite.java:178)
    at junit.framework.TestCase.runBare(TestCase.java:129)
    at junit.framework.TestResult.protect(TestResult.java:100)
    at junit.framework.TestResult.runProtected(TestResult.java:117)
    at junit.framework.TestResult.run(TestResult.java:103)
    at junit.framework.TestCase.run(TestCase.java:120)
    at junit.framework.TestSuite.run(TestSuite.java, Compiled Code)
    at junit.ui.TestRunner2.run(TestRunner.java:429)

这个堆栈跟踪信息相当不详细,它只表明测试用例无法实例化。它没有详细说明原始错误的位置或起源。这种缺乏信息使得难以推断异常的根本原因。

不要在构造函数中设置数据,而是通过覆盖setUp()来执行测试设置。任何在setUp()中抛出的异常都将被正确报告。将此堆栈跟踪与前一个示例进行比较:

java.lang.IllegalStateException: Oops
    at bp.DTC.setUp(DTC.java:34) 
    at junit.framework.TestCase.runBare(TestCase.java:127)
    at junit.framework.TestResult.protect(TestResult.java:100)
    at junit.framework.TestResult.runProtected(TestResult.java:117)
    at junit.framework.TestResult.run(TestResult.java:103)
    ...

这个堆栈跟踪信息更加详细,它显示了抛出的异常(IllegalStateException)以及其来源。这使得解释测试设置的失败要容易得多。


1
@Pascal: 谢谢,是的,我将我的初始化代码移动到setup()中,错误消息从 “junit.framework.AssertionFailedError: Cannot instantiate test case” 变成了: “在类路径资源[net/ezswitch/registration/ComponentContext.xml]中定义的名称为'transactionManager'的bean创建错误:在设置bean属性'sessionFactory'时无法解析对bean'sessionFactory'的引用” - Henrique Ordine

30

在工作中,我们发现了一些非常有趣的东西,可以回答你的问题。当你运行一个测试套件,特别是一个大型的测试集(200+),JUnit开始使用大量的内存。这是因为在任何实际的测试方法运行之前,所有的测试都会被实例化。

由于我们使用Spring来连接一些JPA EntityManager对象进行数据库测试,因此这变成了许多对象和大量的内存,而且在测试进行到一半时,我们会收到OutOfMemory异常,这就产生了“内存泄漏”的问题。

在我看来,最佳实践是使用setUp和tearDown来注入你的依赖项,并将所有类引用设置为空值。这将使你的测试运行更快,节省很多麻烦!

希望你从我们的错误中吸取教训 :)


25

以下是三个很好的理由:

  1. 有些情况下可能更喜欢在测试用例执行之前尽可能晚地设置测试夹具。

  2. 某些测试用例可能是深层次的测试用例继承结构的一部分。在整个构造函数层次结构完成之前延迟设置测试夹具可能更可取。

  3. 如果setUp()中的设置代码失败,您可以获得更好的诊断结果,而不是在构造函数中失败。

1. 将设置夹具的时间延迟到测试用例之前

设计易用性
http://www.artima.com/weblogs/viewpost.jsp?thread=70189

......正如Elliotte Rusty Harold所说,如果您要为每个测试方法创建一个新的TestCase实例,“为什么还要费心思写setUp()方法呢?” 您可以直接使用TestCase构造函数。

我听过Bruce Eckel指出在setUp()中创建夹具与在TestCase构造函数中创建夹具之间存在“一个微妙的区别”。JUnit会“预先创建所有的TestCase实例”,然后对于每个实例,依次调用setup()、测试方法和tearDown()。换句话说,“微妙的区别在于构造函数都是批量调用的,而setUp()方法则会在每个测试方法之前被调用”。但实践中似乎并不常用。

2. 将设置夹具的时间延迟到所有测试用例实例化之后

ETutorial的Java极限编程-4.6 设置和拆卸
http://etutorials.org/Programming/Java+extreme+programming/Chapter+4.+JUnit/4.6+Set+Up+and+Tear+Down/

你可能会想知道为什么要编写setUp()方法,而不是在测试用例的构造函数中初始化字段。毕竟,由于每个测试方法都会创建测试用例的新实例,因此构造函数总是在setUp()之前被调用。在绝大多数情况下,您可以使用构造函数代替setUp(),而不会产生任何副作用。

但是如果您的测试用例是更深层次的继承层次结构的一部分,则可能希望将对象初始化推迟到衍生[测试]类的实例完全构造完成时。这是您可能希望使用setUp()而不是构造函数进行初始化的一个很好的技术原因。使用setUp()和tearDown()还有文档目的好处,因为它可以使代码更易读

3. 在设置失败的情况下提供更好的诊断信息

JUnit最佳实践(JavaWorld)
http://www.javaworld.com/jw-12-2000/jw-1221-junit.html

在构造函数中设置测试用例并不是一个好主意。...

假设[在测试用例构造函数中进行设置],在执行设置时,设置代码抛出IllegalStateException异常。作为回应,JUnit将抛出AssertionFailedError异常,表明无法实例化测试用例。...

[在测试用例构造函数中设置代码抛出异常的]堆栈跟踪信息相当无用;它只表示无法实例化测试用例。

不要在构造函数中设置数据,通过覆盖setUp()方法执行测试设置。任何在setUp()方法中抛出的异常都将被正确报告。...

这个堆栈跟踪[在setUp()方法中抛出异常而不是在测试用例构造函数中抛出]更加详细; 它显示了抛出的异常(IllegalStateException)和其来源。这使得更容易解释测试设置的失败原因。


我并不完全同意这个说法:“在测试用例是深层继承层次结构的一部分的情况下,您可能希望推迟对象初始化,直到派生[测试]类的实例完全构造出来。” - Ajoy Bhatia
我不同意这个说法:“在测试用例是深度继承层次结构的一部分的情况下,...派生类[测试]会被完全构造。” 这难道不等于说在一个类是深度继承层次结构的一部分时,我们可能希望将对象初始化推迟到派生类的字段完全构造之后吗?也就是说,用“测试用例”替换“类”,这听起来很荒谬。超类的构造函数总是首先被调用。它对派生类的字段/方法一无所知。难道超类的构造函数不能做任何事情吗? - Ajoy Bhatia
我同意@AjoyBhatia的观点。当然,如果你要从构造函数中调用可重写方法,如果子类中的方法假定子类构造函数已经被调用(实际上并没有),那么你可能会遇到问题。但是对于普通类来说也是一样的,在这种情况下(如果你不能将方法设置为final),你可能想使用一个init() / setUp()方法。 - herman

6

SpringJUnit4ClassRunner 这样的自定义运行器可能需要在构造函数和 @Before 方法之间运行一些代码。在这种情况下,运行器可以注入一些依赖项,这些依赖项是 @Before 方法所需的。但是依赖注入只能在对象构造完成后运行。


3
您需要这样做的原因是,对于许多测试,您经常需要在每个测试之前初始化状态,以便测试可以做出关于它们运行的起始状态的假设。
假设您的测试类包装了数据库访问。每次测试后,您都希望删除测试对数据库所做的任何更改 - 如果不这样做,每个测试都会针对略微修改的数据库运行。此外,如果先前的一些测试失败,则任何给定的测试可能会看到不同的更改集。例如,假设test1进行插入,test2检查您是否准确读取了表大小。第1天,test1失败,0是正确的。第2天,test1成功,1是正确的?
顺便说一下,JUnit还支持@BeforeClass,如果您想进行全局设置,则设置和拆卸是可选的。

6
我认为 OP 并没有质疑设置代码的有用性。相反,问题是为什么我们不直接使用类构造函数来完成 setUp 所做的工作。 - Nader Shirazie

-4

我认为应该有以下一些原因:

  1. 如果您将@Before的内容移动到构造函数中,那么可以,但是@After的内容应该放在哪里呢?
  2. 构造函数和@Before/@After的区别在于,构造函数应该用于实例化类的某些内容,而@Before/@After则用于准备测试用例资源。

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