【问题标题】:Is it really necessary to nullify objects in JUnit teardown methods? [duplicate]真的有必要在 JUnit 拆解方法中取消对象吗? [复制]
【发布时间】:2015-03-27 10:32:39
【问题描述】:

answer 引起了我对类似问题的兴趣。我相信这是不正确的。所以我创建了一些测试代码。我的问题是,这段代码是否证明/反驳/不确定在拆卸方法中使成员变量无效的假设?我用 JUnit4.8.1 测试过。

JUnit 为 4 个测试中的每一个创建一个新的测试类实例。每个实例都包含一个 Object obj。这个 obj 也作为静态 WeakHashMap 的键插入。如果并且当 JUnit 释放其对测试实例的引用时,关联的 obj 值将变为弱引用,因此符合 gc 条件。该测试试图强制执行 gc。 WeakHashMap 的大小会告诉我 obj 是否被 gc'ed。一些测试使 obj 变量无效,而另一些则没有。

import org . junit . Before ;
import org . junit . After ;
import org . junit . Test ;
import java . util . ArrayList ;
import java . util . WeakHashMap ;
import java . util . concurrent . atomic . AtomicInteger ;
import static org . junit . Assert . * ;

public class Memory
{
    static AtomicInteger idx = new AtomicInteger ( 0 ) ;

    static WeakHashMap < Object , Object > map = new WeakHashMap < Object , Object > ( ) ;

    int id ;

    Object obj ;

    boolean nullify ;

    public Memory ( )
    {
    super ( ) ;
    }

    @ Before
    public void before ( )
    {
    id = idx . getAndIncrement ( ) ;
    obj = new Object ( ) ;
    map . put ( obj , new Object ( ) ) ;
    System . out . println ( "<BEFORE TEST " + id + ">" ) ;
    }

    void test ( boolean n )
    {
    nullify = n ;
    int before = map . size ( ) ;
    gc ( ) ;
    int after = map . size ( ) ;
    System . out . println ( "BEFORE=" + before + "\tAFTER=" + after ) ;
    }

    @ Test
    public void test0 ( )
    {
    test ( true ) ;
    }

    @ Test
    public void test1 ( )
    {
    test ( false ) ;
    }

    @ Test
    public void test2 ( )
    {
    test ( true ) ;
    }

    @ Test
    public void test3 ( )
    {
    test ( false ) ;
    }

    @ After
    public void after ( )
    {
    if ( nullify )
        {
        System . out . println ( "Nullifying obj" ) ;
        obj = null ;
        }
    System . out . println ( "<AFTER TEST " + id + ">" ) ;
    }

    /**
     * Try to force a gc when one is not really needed.
     **/
    void gc ( )
    {
    ArrayList < Object > waste = new ArrayList < Object > ( ) ;
    System . gc ( ) ; // only a suggestion but I'll try to force it
    list :
    while ( true ) // try to force a gc
        {
        try
            {
            waste . add ( new Object ( ) ) ;
            }
        catch ( OutOfMemoryError cause )
            {
            // gc forced? should have been
            waste = null ;
            break list ;
            }
        }
    System . gc ( ) ; // only a suggestion but I tried to force it
    }
}

我使用命令行界面运行代码(利用 -Xmx128k 选项增加垃圾回收)并得到以下结果

.<BEFORE TEST 0>
BEFORE=1    AFTER=1
Nullifying obj
<AFTER TEST 0>
.<BEFORE TEST 1>
BEFORE=2    AFTER=1
<AFTER TEST 1>
.<BEFORE TEST 2>
BEFORE=2    AFTER=1
Nullifying obj
<AFTER TEST 2>
.<BEFORE TEST 3>
BEFORE=2    AFTER=1
<AFTER TEST 3>

Test0 obj 无效,在 Test1 中它被 gc'ed。但是 Test1 obj 没有被取消,它在 Test2 中被 gc'ed。这表明不需要使对象无效。

【问题讨论】:

    标签: java junit


    【解决方案1】:

    JUnit 4.x 风格的测试和测试套件处理此问题的方式与 JUnit 3.x 测试套件不同。

    简而言之,您应该在 JUnit3 风格的测试中将字段设置为 null,但在 JUnit4 风格的测试中不需要

    使用 JUnit 3.x 样式测试,TestSuite 包含对其他 Test 对象(可能是 TestCase 对象或其他 TestSuite 对象)的引用。如果您创建一个包含许多测试的套件,那么在最外层套件的整个运行过程中,将有对所有叶 TestCase 对象的硬引用。如果您的某些TestCase 对象在setUp() 中分配了占用大量内存的对象,并且对这些对象的引用存储在tearDown() 中未设置为null 的字段中,那么您可能会遇到内存问题。

    换句话说,对于 JUnit 3.x 风格的测试,运行哪些测试的规范引用了实际的 TestCase 对象。在测试运行期间,任何可从TestCase 对象访问的对象都将保存在内存中。

    对于 JUnit 4.x 样式测试,运行哪些测试的规范使用 Description 对象。 Description 对象是一个值对象,它指定要运行什么,但不指定如何运行它。测试由Runner 对象运行,该对象采用测试或套件的Description 并确定如何执行测试。甚至将测试状态通知给测试侦听器也使用了Description 对象。

    JUnit4 测试用例的默认运行器JUnit4 仅在该测试运行期间保留对测试对象的引用。如果您使用自定义运行器(通过 @RunWith 注释),则该运行器可能会或可能不会将测试引用保留更长时间。

    也许您想知道如果在 JUnit4 样式的 Suite 中包含 JUnit3 样式的测试类会发生什么? JUnit4 将调用new TestSuite(Class),这将为每个测试方法创建一个单独的TestCase 实例。运行者将在测试运行的整个生命周期中保留对 TestSuite 的引用。

    简而言之,如果您正在编写 JUnit4 风格的测试,则不必担心在拆解时将测试用例的字段设置为 null(当然,要使用免费资源)。如果您正在编写 JUnit3 样式的测试,在 setUp() 中分配大对象并将这些对象存储在 TestCase 的字段中,请考虑将字段设置为 null

    【讨论】:

    • 这是最好的答案。在 JUnit 3.x 中需要进行无效化(以避免内存泄漏)。如果您使用 JUnit4.x 并且默认运行器无效化是不必要的。如果您使用自定义跑步者,那么您可能需要取消。
    【解决方案2】:

    不,没有必要。

    拆卸方法适用于喜欢显式关闭、终止、关闭、处置、断开连接、取消注册或其他方式的生命周期对象。

    即使您的引用在下一个测试用例中仍然存在,它们也会被您的设置方法覆盖并变为未引用,从而有资格进行垃圾回收。

    如果 JUnit 为每个方法创建一个新的测试用例实例(似乎是这种情况),那么这些测试对象不会被保留。根据一项快速实验,如果测试通过,至少不会。所以无论如何都会在闲暇时收集它。

    【讨论】:

    • JUnit3 样式测试和 JUnit4 样式测试都会为每个执行的测试方法创建一个单独的对象,因此除非引用存储在静态(不推荐)中,否则引用不会被下一组覆盖 -向上方法
    • @NamshubWriter 是保证还是实现细节?如果是后者,那么它可能会改变(尽管不太可能)。无论如何,这些实例不会保留(根据实验,在 JUnit 4.8.1 中)——至少在测试通过时不会保留。
    • @NamshubWriter 我更新了我的答案。感谢您提供信息。
    • 这不是实现细节,而是记录在案的行为。 JUnit 这样做是为了让一个测试的结果影响另一个测试的行为变得更加困难。
    猜你喜欢
    • 2020-05-22
    • 2015-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-05
    • 2021-01-25
    • 1970-01-01
    • 2014-06-02
    相关资源
    最近更新 更多