最佳实践:在setUp()或声明中初始化JUnit类字段?


120

我应该在这样的声明中初始化类字段吗?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

还是像这样在setUp()中?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

我倾向于使用第一种形式,因为它更简洁,并且允许我使用最终字段。如果我不需要使用setUp()方法进行设置,是否仍应使用它,为什么?

澄清: JUnit将每个测试方法实例化一次测试类。这意味着list无论我在哪里声明,每次测试都会创建一次。这也意味着测试之间没有时间依赖性。因此,使用setUp()似乎没有任何优势。但是,JUnit FAQ中有许多示例可以在setUp()中初始化一个空集合,因此我认为一定有原因。


2
注意,答案在JUnit 4(在声明中初始化)和JUnit 3(使用setUp)中有所不同。这是造成混乱的根源。
Nils von Barth

Answers:


99

如果您特别想知道JUnit FAQ中的示例,例如基本测试模板,那么我认为展示的最佳实践是被测类。应该在setUp方法(或测试方法)中实例化。

当JUnit示例在setUp方法中创建ArrayList时,它们都将继续测试ArrayList的行为,并使用诸如testIndexOutOfBoundException,testEmptyCollection之类的情况。那里的观点是有人编写课程并确保其正常工作。

在测试自己的类时,您可能应该做同样的事情:在setUp或测试方法中创建对象,以便稍后将其破坏时可以获得合理的输出。

另一方面,如果在测试代码中使用Java集合类(或其他库类),则可能不是因为您要对其进行测试-它只是测试夹具的一部分。在这种情况下,您可以放心地假定它可以按预期工作,因此在声明中对其进行初始化不会有问题。

对于它的价值,我使用的是一个相当大的,由TDD开发的,已有几年历史的代码库。我们习惯性地在测试代码中的声明中初始化事物,而在我从事该项目的一年半中,它从未引起任何问题。因此,至少有一些轶事证据表明这样做是合理的。


45

我开始进行自我挖掘,发现使用的一个潜在优势setUp()。如果在执行过程中抛出任何异常setUp(),JUnit将打印出非常有用的堆栈跟踪。另一方面,如果在对象构造过程中引发了异常,则错误消息仅表示JUnit无法实例化测试用例,并且您看不到发生故障的行号,这可能是因为JUnit使用反射来实例化测试类。

所有这些都不适用于创建空集合的示例,因为它永远不会抛出,但这是该setUp()方法的优点。


18

除了Alex B的答案。

甚至需要使用setUp方法在特定状态下实例化资源。在构造函数中执行此操作不仅是时间问题,而且由于JUnit运行测试的方式,在运行一个测试后,每个测试状态都将被擦除。

JUnit首先为每个测试方法创建testClass实例,并在创建每个实例后开始运行测试。在运行测试方法之前,先运行其设置方法,在此状态下可以准备一些状态。

如果将在构造函数中创建数据库状态,则所有实例将在运行每个测试之前彼此实例化db状态。从第二次测试开始,测试将在脏状态下运行。

JUnits生命周期:

  1. 为每个测试方法创建一个不同的testclass实例
  2. 对每个测试类实例重复上述步骤:调用设置+调用测试方法

在使用两种测试方法进行测试的一些日志记录中,您将获得:(数字为哈希码)

  • 创建新实例:5718203
  • 创建新实例:5947506
  • 设定:5720203
  • TestOne:5720203
  • 设置:5947506
  • 测试二:5947506

3
正确,但没有主题。数据库本质上是全局状态。这不是我面临的问题。我只关心适当独立测试的执行速度。
克雷格·莫特林

此初始化顺序仅在JUnit 3中适用,这是一个重要警告。在JUnit 4中,测试实例是延迟创建的,因此在声明或设置方法中进行初始化都在测试时进行。同样对于一次性设置,可以使用@BeforeClassJUnit 4中
尼尔斯·冯·巴特

11

在JUnit 4中:

  • 对于被测类,在@Before方法中进行初始化以捕获失败。
  • 对于其他班级,请在声明中初始化...
    • ...为了简洁起见,并标记字段 final完全如问题中所述,
    • ...除非复杂的初始化可能失败(在这种情况下使用use @Before)以捕获失败。
  • 对于全局状态(尤其是慢速初始化,如数据库),请使用@BeforeClass,但要注意测试之间的依赖性。
  • 当然,应该在测试方法本身中完成在单个测试中使用的对象的初始化。

@Before方法或测试方法中进行初始化可以使您更好地报告故障。这对于实例化被测类(可能会中断)特别有用,但对调用外部系统(如文件系统访问(“找不到文件”)或连接到数据库(“连接被拒绝”))也很有用。

拥有简单的标准并始终使用是可以接受的@Before在声明(清楚的错误,但详细)或总是初始化(简洁但给人混乱的错误),因为复杂的编码规则是难以遵循,这是不是一个大问题。

初始化setUp是JUnit 3的遗物,所有测试实例都被急切地初始化,如果进行昂贵的初始化,则会导致问题(速度,内存,资源耗尽)。因此,最佳实践是在中进行昂贵的初始化setUp,该初始化仅在执行测试时运行。这不再适用,因此使用的必要性大大降低setUp

这总结了埋葬线索的其他几个答复,特别是Craig P. Motlin(问题本身和自我回答),Moss Collum(正在测试的班级)和dsaff。


7

在JUnit 3中,您的字段初始化程序将在每种测试方法运行一次之前运行。只要您的字段值在内存中很小,设置时间很少并且不影响全局状态,使用字段初始化程序在技术上就可以了。但是,如果这些设置不成立,则可能会在运行第一个测试之前最终消耗大量内存或时间来设置字段,甚至可能耗尽内存。因此,许多开发人员总是在setUp()方法中设置字段值,即使在并非绝对必要的情况下,该方法也始终是安全的。

请注意,在JUnit 4中,测试对象初始化是在测试运行之前进行的,因此使用字段初始化器是更安全和推荐的样式。


有趣。因此,您最初描述的行为仅适用于JUnit 3?
Craig P. Motlin 2011年

6

在您的情况下(创建列表),实践上没有区别。但是通常最好使用setUp(),因为这将帮助Junit正确报告异常。如果在Test的构造器/初始化器中发生异常,则表示Test Failure。但是,如果在设置过程中发生异常,自然会在设置测试时将其视为某些问题,并且junit会适当地报告它。


1
说得好。只需习惯于始终在setUp()中实例化,就不用担心一个问题了-例如,我应该在哪里实例化fooBar,在哪里集合。这是您只需要遵循的一种编码标准。您不会从列表中受益,而会从其他实例中受益。
奥拉夫·科克

@Olaf感谢您提供有关编码标准的信息,我还没有考虑过。不过,我倾向于同意Moss Collum关于编码标准的想法。
Craig P. Motlin,09年

5

我更喜欢可读性,它通常不使用设置方法。当基本设置操作花费很长时间并且在每次测试中都重复执行时,我会例外。
那时,我使用@BeforeClass注释将功能转移到设置方法中(稍后进行优化)。

使用@BeforeClass设置方法进行优化的示例:我将dbunit用于某些数据库功能测试。设置方法负责将数据库置于已知状态(非常慢... 30秒-2分钟,具体取决于数据量)。我在带有注释的设置方法中加载此数据,@BeforeClass然后针对同一组数据运行10-20个测试,而不是在每个测试中重新加载/初始化数据库。

使用Junit 3.8(扩展您的示例中所示的TestCase)需要编写的代码比添加注释要多一些,但是“在类设置之前运行一次”仍然是可能的。


1
+1是因为我也更喜欢可读性。但是,我完全不相信第二种方法是优化。
2009年

@Motlin我添加了dbunit示例,以阐明如何使用安装程序进行优化。
Alex B

数据库本质上是全局状态。因此,将数据库设置移动到setUp()并不是一项优化,测试必须正确完成。
Craig P. Motlin,09年

@Alex B:正如Motlin所说,这不是优化。您只是在更改代码在何处完成初始化,而不是更改次数或执行速度。
Eddie

我打算暗示使用“ @BeforeClass”注释。编辑示例进行澄清。
Alex B

2

由于每个测试都是使用对象的新实例独立执行的,因此没有什么意义指向Test对象具有任何内部状态,只是在setUp()和单个测试之间共享tearDown()。这是(除了其他人提供的原因之外)使用此setUp()方法的一个好原因。

注意:对于JUnit测试对象而言,保持静态是一个坏主意!如果出于跟踪或诊断目的以外的目的在测试中使用静态变量,则将使JUnit的目的无效,这就是说测试可以(可以)以任何顺序运行,每个测试都以新鲜,干净的状态。

使用setUp()它的好处是您不必在每种测试方法中都剪切和粘贴初始化代码,并且构造函数中没有测试设置代码。在您的情况下,几乎没有区别。只需显示一个空列表,就可以安全地完成它,也可以在构造函数中完成,因为这是一个简单的初始化。但是,正如您和其他人所指出的,任何可能引发错误的操作Exception都应在setUp()以便在诊断堆栈转储失败时进行诊断。

在您的情况下,您只是在创建一个空列表,我将按照您建议的方式进行操作:在声明时分配新列表。尤其是因为final如果您的测试课程有意义,您可以选择标记它。


1
+1是因为您是第一个真正在对象构建过程中真正支持初始化列表以将其标记为最终的人。但是,有关静态变量的内容是这个问题的题外话。
Craig P. Motlin,2009年

@Motlin:是的,有关静态变量的内容有些不合时宜。我不确定为什么要添加此内容,但是在当时看来,这是对我在第一段中所说的内容的扩展。
Eddie

final问题中提到了的优点。
尼尔斯·冯·巴特

0
  • 常数值(用于固定装置或断言中)应在其声明中初始化,并且final(永不更改)

  • 被测试的对象应该在setup方法中初始化,因为我们可能会将其设置为on。当然,我们可能现在不设置某些内容,但可以稍后进行设置。实例化init方法可以简化更改。

  • 如果测试对象的依赖项是模拟的,则它们甚至都不应该自己实例化:今天,模拟框架可以通过反射来实例化它。

不依赖模拟的测试可能看起来像:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

具有要隔离的依赖关系的测试可能看起来像:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.