ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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 常见问题解答中有许多在 setUp() 中初始化空集合的示例,所以我认为一定是有原因的。

请注意,JUnit 4(在声明中初始化)和 JUnit 3(使用 setUp)的答案不同;这是混乱的根源。

M
Moss Collum

如果您特别想知道 JUnit 常见问题解答中的示例,例如 basic test template,我认为那里展示的最佳实践是应该在您的 setUp 方法中实例化 被测类 (或在测试方法中)。

当 JUnit 示例在 setUp 方法中创建 ArrayList 时,它们都会继续测试该 ArrayList 的行为,例如 testIndexOutOfBoundException、testEmptyCollection 等。有人编写课程并确保其正常工作的观点。

在测试您自己的类时,您可能应该这样做:在 setUp 或测试方法中创建您的对象,这样如果您以后破坏它,您将能够获得合理的输出。

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

对于它的价值,我在一个相当大的、有几年历史的、由 TDD 开发的代码库上工作。我们习惯性地在测试代码的声明中初始化东西,在我从事这个项目的一年半里,它从未引起过问题。所以至少有一些轶事证据表明这是一件合理的事情。


C
Craig P. Motlin

我开始挖掘自己,发现使用 setUp() 的一个潜在优势。如果在执行 setUp() 期间抛出任何异常,JUnit 将打印一个非常有用的堆栈跟踪。另一方面,如果在对象构造期间抛出异常,错误消息只是说 JUnit 无法实例化测试用例并且您看不到发生失败的行号,可能是因为 JUnit 使用反射来实例化测试类。

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


J
Jurgen Hannaert

除了亚历克斯 B 的回答。

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

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

如果数据库状态将在构造函数中创建,则所有实例将在运行每个测试之前立即实例化数据库状态。在第二个测试中,测试将以脏状态运行。

JUnit 生命周期:

为每个测试方法创建不同的测试类实例 对每个测试类实例重复:调用设置 + 调用测试方法

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

创建新实例:5718203

创建新实例:5947506

设置:5718203

测试一:5718203

设置:5947506

测试二:5947506


正确,但题外话。数据库本质上是全局状态。这不是我面临的问题。我只关心正确独立测试的执行速度。
这个初始化顺序只在 JUnit 3 中是正确的,这是一个重要的警告。在 JUnit 4 中,测试实例是延迟创建的,因此在声明或设置方法中进行初始化都发生在测试时。同样对于一次性设置,可以在 JUnit 4 中使用 @BeforeClass
N
Nils von Barth

在 JUnit 4 中:

对于被测类,在 @Before 方法中进行初始化,以捕获失败。

对于其他类,在声明中初始化... ...为简洁起见,并将字段标记为最终,正如问题中所述,...除非它是可能失败的复杂初始化,在这种情况下使用@Before捕捉失败。

...为简洁起见,并将字段标记为最终,完全如问题中所述,

...除非是可能失败的复杂初始化,在这种情况下使用@Before 来捕获失败。

对于全局状态(尤其是缓慢的初始化,如数据库),使用@BeforeClass,但要注意测试之间的依赖关系。

单个测试中使用的对象的初始化当然应该在测试方法本身中完成。

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

可以接受有一个简单的标准并始终使用 @Before(明确错误但冗长)或始终在声明中初始化(简洁但给出令人困惑的错误),因为难以遵循复杂的编码规则,并且这没什么大不了的。

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

这总结了其他几个埋没问题的回复,特别是 Craig P. Motlin(问题本身和自我回答)、Moss Collum(被测类)和 dsaff。


N
Nils von Barth

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

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


有趣的。那么您最初描述的行为仅适用于 JUnit 3?
a
amit

在您的情况下(创建列表)在实践中没有区别。但通常最好使用 setUp(),因为这将有助于 Junit 正确报告异常。如果在测试的构造函数/初始化程序中发生异常,那就是测试失败。但是,如果在设置过程中出现异常,自然会认为这是设置测试的问题,而 junit 会适当地报告它。


说得好。只要习惯于总是在 setUp() 中实例化,你就不用担心一个问题——例如,我应该在哪里实例化我的 fooBar,我的集合在哪里。这是一种您只需要遵守的编码标准。使您受益的不是列表,而是其他实例。
@Olaf 感谢您提供有关编码标准的信息,我没有想到这一点。不过,我更倾向于同意 Moss Collum 关于编码标准的想法。
A
Alex B

我首先更喜欢可读性,它通常不使用设置方法。当基本设置操作需要很长时间并且在每个测试中重复时,我会例外。
此时,我使用 @BeforeClass 注释将该功能移动到设置方法中(稍后进行优化)。

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

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


+1,因为我也更喜欢可读性。但是,我根本不相信第二种方式是一种优化。
@Motlin 我添加了 dbunit 示例以阐明如何使用设置进行优化。
数据库本质上是全局状态。因此将数据库设置移动到 setUp() 不是优化,测试正确完成是必要的。
@Alex B:正如 Motlin 所说,这不是优化。您只是在更改代码中初始化完成的位置,而不是多少次或多快。
我打算暗示使用“@BeforeClass”注释。编辑示例以澄清。
E
Eddie

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

注意:让 JUnit 测试对象保持静态状态是个坏主意!如果您在测试中将静态变量用于跟踪或诊断目的之外的任何其他目的,那么您将使 JUnit 的部分目的无效,即测试可以(并且可以)以任何顺序运行,每个测试都以清新、干净的状态。

使用 setUp() 的优点是您不必在每个测试方法中剪切和粘贴初始化代码,并且您在构造函数中没有测试设置代码。在你的情况下,差别不大。只需创建一个空列表就可以安全地显示它或在构造函数中完成,因为它是一个微不足道的初始化。但是,正如您和其他人所指出的,任何可能引发 Exception 的事情都应该在 setUp() 中完成,以便在失败时获得诊断堆栈转储。

在您的情况下,您只是创建一个空列表,我会按照您的建议进行操作:在声明点分配新列表。特别是因为如果这对您的测试类有意义,您可以选择将其标记为 final


+1,因为您是第一个真正支持在对象构造期间初始化列表以将其标记为最终的人。不过,关于静态变量的内容与问题无关。
@Motlin:是的,关于静态变量的东西有点离题。我不确定我为什么要添加它,但当时它似乎是合适的,是我在第一段中所说的话的延伸。
不过,问题中提到了 final 的优势。
d
davidxxx

常量值(在固定装置或断言中使用)应该在它们的声明和最终值中初始化(因为永远不会改变)

被测对象应该在 setup 方法中初始化,因为我们可以设置一些东西。当然,我们现在可能不设置某些东西,但我们可以稍后设置。在 init 方法中实例化将简化更改。

如果这些被模拟对象的依赖关系,甚至不应该由你自己实例化:今天模拟框架可以通过反射来实例化它。

不依赖于 mock 的测试可能如下所示:

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()
         ...
    }
}

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅