JUnit: 使用构造函数而不是 @Before

100
我正在使用JUnit 4。我看不出在构造函数中初始化和使用一个专用的带有@Before注解的init函数之间有什么区别。这是否意味着我不必担心它?
是否有任何情况下@Before比仅在构造函数中进行初始化更有用?

1
可能是setUp/tearDown (@Before/@After)的重复问题,为什么我们需要它们在JUnit中? - Ciro Santilli OurBigBook.com
参见:https://dev59.com/vHRB5IYBdhLWcg3wyqEd - Craig P. Motlin
你确定你要用 @Before,而不是 @BeforeClass 吗?在这里查看它们的区别:https://dev59.com/mWIj5IYBdhLWcg3waEZe。 - Line
8个回答

110

不,使用构造函数来初始化JUnit测试夹具在技术上等同于使用@Before方法(因为JUnit为每个@Test创建了测试类的新实例)。唯一的(内涵)差异是它破坏了@Before@After之间的对称性,这可能会让一些人感到困惑。在我看来,最好遵循约定(即使用@Before)。

还要注意,在JUnit 4和注释之前,有专用的setUp()tearDown()方法- @Before@After注释替换了这些方法,但保留了底层逻辑。因此,对于从JUnit 3或更早版本迁移的人来说,使用注释也会使生活更轻松。

显著差异

下面是从评论中得到的更多细节:

  • @Before允许覆盖父类行为,构造函数则强制您调用父类构造函数
  • 构造函数在子类构造函数和@Rule方法之前运行,@Before在所有这些方法之后运行
  • @Before中的异常会导致调用@After方法,构造函数中的异常则不会

2
@Before 不会被多次调用吗?(每个测试方法之前?) - sly7_7
3
由于JUnit为每个测试方法创建一个新的测试类实例,因此构造函数和@Before方法被调用的次数完全相同。 - Péter Török
10
同时,我已经了解到一个重要的区别:如果在构造函数中抛出异常,那么就没有对象可以运行@After。因此,我仍然使用注解,并仅保留最小的构造函数。 - vbence
2
@DerekMahar,更不用说并非所有的xUnit克隆在这方面的行为都是相同的。例如,NUnit不会为每个测试方法执行创建一个新的测试类实例,因此在那里使用构造函数而不是[SetUp]方法(NUnit中与@Before相当)实际上会导致错误。对我来说,这就像“依赖于接口,而不是实现”的格言一样,在长期维护中使代码更具可移植性和易于维护。 - Péter Török
2
@PéterTörök,我不知道NUnit没有遵循这个惯例。我认为你已经说服我使用@Before是一个好主意,尽管当使用@Before时,我无法使我的数据成员引用最终化,这让我感到困扰。 - Derek Mahar
显示剩余5条评论

32

@Before在某些情况下更有意义,因为它会在类的构造函数之后被调用。当您使用像Mockito这样带有@Mock注释的模拟框架时,这种差异非常重要,因为您的@Before方法将在模拟对象初始化之后被调用。然后,您可以使用模拟对象来提供类的构造函数参数以进行测试。

在使用协作bean的单元测试中,我发现这是一个非常常见的模式。

这里是一个(虽然牵强附会)的例子:

@RunWith(MockitoJUnitRunner.class)
public class CalculatorTest {
    @Mock Adder adder;
    @Mock Subtractor subtractor;
    @Mock Divider divider;
    @Mock Multiplier multiplier;

    Calculator calculator;

    @Before
    public void setUp() {
        calculator = new Calculator(adder,subtractor,divider,multiplier);
    }

    @Test
    public void testAdd() {
        BigDecimal value = calculator.add(2,2);
        verify(adder).add(eq(2),eq(2));
    }
}

1
为什么不能在构造函数中做同样的事情呢?在构造函数中,只需使用测试运行器应该首先初始化的所有模拟对象来初始化计算器。 - Derek Mahar
此外,你定义了这些 Mocks 为 @Nonnull。你需要一个构造函数来解决这个问题。setUp() 方法行不通,因为你可以在另一个测试中实例化 CalculatorTest 类,这将导致 NPE。 - Benjamin Marwell

14

我更喜欢使用构造函数来初始化我的测试对象,因为这样可以使所有成员变量都是final,这样IDE或编译器会在构造函数忘记初始化某个成员变量时告诉我,并防止另一个方法设置它们。

依我之见,@Before违反了最重要的Java约定之一,即依赖构造函数完全初始化对象!

JUnit 5也对构造函数注入提供了更好的支持。


5

我更喜欢将我的固定装置声明为final,并在内联或构造函数中初始化它们,这样我就不会忘记初始化它们!然而,由于@Before中抛出的异常以更用户友好的方式处理,我通常会在@Before中初始化被测试对象。


4

引用自http://etutorials.org/Programming/Java+extreme+programming/Chapter+4.+JUnit/4.6+Set+Up+and+Tear+Down/

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

在您的测试用例是更深层次继承关系的一部分时,您可能希望推迟对象初始化直到派生类的实例完全构建。这是一个很好的技术原因,您可能希望使用setUp()而不是构造函数进行初始化。使用setUp()和tearDown()还有助于文档编写,因为它可以使代码更易于阅读。


1

有一件事情是构造函数可以实现但@Before不能。

当你需要初始化父类中定义的字段时,必须使用构造函数。例如:

abstract class AbstractIT {
   int fieldAssignedInSubClass;
   public AbstractIT(int fieldAssignedInSubClass) {
      this.fieldAssignedInSubClass= fieldAssignedInSubClass;
   }

   @Before
   void before() {
      // comsume fieldAssignedInSubClass
   } 
}

public class ChildIT extends AbstractIT{
   public ChildIT() {
      // assign fieldAssignedInSubClass by constructor
      super(5566); 
   }

   @Before
   void before() {
      // you cannot assign fieldAssignedInSubClass by a @Before method
   } 
}

0

除了构造函数是唯一可以初始化@Rule对象的方法外,没有任何区别:

public class TestClass {

    @Rule
    public SomeRule rule;

    public TestClass() {
        // code to initialize the rule field
        conf = new RuleConf()
        rule = new SomeRule(conf)
    }
}

0

@Before 有很多使用的理由。它可以使你的测试代码更易读。它与 @After 注解相匹配,后者负责释放已使用的资源,并是 @BeforeClass 注解的对应项。


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