编写第一个JUnit测试

时间:2011-10-05 17:46:48

标签: java unit-testing junit

所以我读过官方的JUnit文档,里面包含了大量的例子,但是(就像许多事情一样)我已经启动了Eclipse,我正在编写我的第一个JUnit测试,而我正在窒息一些基本的设计/概念问题。

因此,如果我的WidgetUnitTest正在测试名为Widget的目标,我假设我需要创建相当数量的Widget以在整个测试方法中使用。我应该在Widget构造函数中还是在WidgetUnitTest方法中构建这些setUp()Widget s与测试方法的比例应该是1:1,还是最佳实践要求尽可能重用Widget

最后,断言/失败和测试方法之间应该存在多少粒度?纯粹主义者可能认为 1-and-1-assertions应存在于测试方法中,但在该范例下,如果Widget有一个名为getBuzz()的getter,我'最终将为getBuzz()提供20种不同的测试方法,其名称为

@Test
public void testGetBuzzWhenFooIsNullAndFizzIsNonNegative() { ... }

与测试多种场景并承载大量断言的1种方法相反:

@Test
public void testGetBuzz() { ... }

感谢一些JUnit大师的见解!

5 个答案:

答案 0 :(得分:17)

模式

有趣的问题。首先 - 我在IDE中配置的终极测试模式:

@Test
public void shouldDoSomethingWhenSomeEventOccurs() throws Exception
{
    //given

    //when

    //then
}

我总是从这个代码开始(聪明人称之为BDD)。

  • given中,我为每个测试设置了唯一的测试设置。

  • when理想情况下是一行 - 您正在测试的内容。

  • then应包含断言。

我不是单个断言提倡者,但是仅测试行为的单个方面。例如,如果该方法应返回某些内容并且还有一些副作用,请创建两个具有相同givenwhen部分的测试。

此外,测试模式还包括throws Exception。这是为了处理Java中烦人的检查异常。如果你测试一些抛出它们的代码,你就不会受到编译器的困扰。当然,如果测试抛出异常就会失败。

设置

测试设置非常重要。一方面,提取公共代码并将其放在setup() / @Before方法中是合理的。但请注意,在阅读测试时(且可读性是单元测试中的最大值!),很容易错过在测试用例开始时悬挂的设置代码。因此,相关的测试设置(例如,您可以以不同的方式创建窗口小部件)应该转到测试方法,但应该提取基础设施(设置常见的模拟,启动嵌入式测试数据库等)。再一次提高可读性。

您是否也知道JUnit会为每个测试创建测试用例类的新实例?因此,即使您在构造函数中创建了CUT( test in ),也会在每次测试之前调用构造函数。有点烦人。

粒度

首先命名您的测试,并考虑您要测试的用例或功能,从不考虑以下方面:

  

这是一个Foo类,有bar()buzz()方法,因此我使用FooTesttestBar()创建testBuzz()。哦,亲爱的,我需要在bar()中测试两条执行路径 - 所以让我们创建testBar1()testBar2()

shouldTurnOffEngineWhenOutOfFuel()很好,testEngine17()很糟糕。

有关命名的更多信息

testGetBuzzWhenFooIsNullAndFizzIsNonNegative名称告诉测试的内容是什么?我知道测试的东西,但为什么呢?你不觉得细节太贴心吗?怎么样:

@Test shouldReturnDisabledBuzzWhenFooNotProvidedAndFizzNotNegative`

它以有意义的方式描述输入和您的意图(假设禁用的buzz 是某种buzz状态/类型)。另请注意,我们不再对getBuzz()的{​​{1}}方法名称和null合同进行硬编码(我们会说:未提供Foo 时)。如果您将来用 null对象模式替换Foo该怎么办?

另外,不要害怕 20种null 的不同测试方法。请考虑您正在测试的20个不同的用例。但是,如果您的测试用例类增长得太大(因为它通常比测试类大得多),请提取几个测试用例。再一次:getBuzz()FooHappyPathTestFooBogusInput都很好,FooCornerCasesFoo1Test都不好。

可读性

争取简短的描述性名称。 Foo2Test中的几行和given中的几行。而已。创建构建器和内部DSL,提取方法,编写自定义匹配器和断言。测试应该比生产代码更具可读性。不要过度嘲笑。

我发现首先编写一系列空的,命名良好的测试用例方法很有用。然后我回到第一个。如果我仍然明白我想在什么条件下测试什么,我在此期间实现构建类API的测试。然后我实现了那个API。聪明人称之为TDD(见下文)。

推荐阅读:

答案 1 :(得分:1)

您将在setup方法中创建一个受测试类的新实例。您希望每个测试能够独立执行,而不必担心另一个先前测试中被测对象中的任何不需要的状态。

我建议您对需要测试的每个场景/行为/逻辑流进行单独测试,而不是对getBuzz()中的所有内容进行大规模测试。您希望每个测试都具有您想要在getBuzz()中验证的内容。

答案 2 :(得分:1)

而不是测试方法尝试专注于测试行为。问一个问题“小部件应该做什么?”然后写一个肯定答案的测试。例如。 “小部件应该小工具”

public void setUp() throws Exception {
   myWidget = new Widget();
}

public void testAWidgetShouldFidget() throws Exception {
  myWidget.fidget();
}

编译,参见“no method fidget defined”错误,修复错误,重新编译测试并重复。接下来问问题每个行为的结果应该是什么,在我们的例子中,fidget的结果是什么?也许有一些可观察的输出就像一个新的2D坐标位置。在这种情况下,我们的小部件将被假定为处于给定位置,当它成为小部件时,它的位置会以某种方式改变。

public void setUp() throws Exception {
   //Given a widget
   myWidget = new Widget();
   //And it's original position
   Point initialWidgetPosition = widget.position();
}


public void testAWidgetShouldFidget() throws Exception {
  myWidget.fidget();
}

public void testAWidgetPositionShouldChangeWhenItFidgets() throws Exception {
  myWidget.fidget();
  assertNotEquals(initialWidgetPosition, widget.position());
}

有些人会反对这两种测试运行相同的fidget行为,但是有必要单独指出fidget的行为,而不管它如何影响widget.position()。如果一个行为中断,单个测试将查明失败的原因。此外,重要的是要声明行为可以单独执行作为规范的实现(你确实有程序规范吗?),这表明你需要一个烦躁的小部件。最后,所有关于将程序规范实现为运行界面的代码,这些代码既展示了您已完成规范,又展示了如何与您的产品进行交互。这实际上是TDD应该如何工作的。任何解决错误或测试产品的尝试通常会导致对使用哪个框架,覆盖范围以及套件应该如何精细化的无意义争论。每个测试用例都应该是将您的规范分解为一个组件,您可以使用Given / When / Then开始短语。给定{某个应用程序状态或前置条件}当{调用行为时}然后{断言一些可观察的输出}。

答案 3 :(得分:0)

首先,setUp和tearDown方法将在每次测试之前和之后调用,因此setUp方法应该创建对象,如果在每次测试中都需要它们,并且测试特定的东西可以在测试中完成本身。

其次,由您决定如何测试您的程序。显然,您可以为程序中的每种可能情况编写测试,并最终对每种方法进行大量测试。或者你可以为每个方法只编写一个测试,它会检查每个可能的场景。我会建议两种方式之间的混合。你真的不需要测试琐碎的getter / setter,但是如果测试失败,只为一个方法编写一个测试可能会导致混淆。您应该决定哪些方法值得测试,以及哪些方案值得测试。但原则上每个场景都应该有自己的测试。

大多数情况下,我的测试代码覆盖率达到80%到90%。

答案 4 :(得分:0)

我完全是第二个Tomasz Nurkiewicz回答,所以我会说,而不是重复他说的一切。

还有几点:

不要忘记测试错误情况。你可以考虑这样的事情:

@Test
public void throwExceptionWhenConditionOneExist() {
    // setup
    // ...
    try {
       classUnderTest.doSomething(conditionOne);
       Assert.fail("should have thrown exception");
    } catch (IllegalArgumentException expected) {
       Assert.assertEquals("this is the expected error message", expected.getMessage());
    } 
}

此外,在考虑被测课程的设计之前,开始编写测试具有很大的价值。如果你是单元测试的初学者,我不能强调同时学习这种技术(这被称为TDD,测试驱动开发),就像这样:

  • 您考虑用户需求的用户案例
  • 你为它写了一个基本的第一个测试
  • 你进行编译(通过创建所需的类 - 包括你的测试类等)
  • 你运行它:它应该失败
  • 现在,您实现了测试类的功能,使其通过(并且不再是
  • 冲洗,并重复新要求

当您的所有要求都通过测试时,您就完成了。你永远不要在你的生产代码中写任何没有测试的东西(例外情况是记录代码,而不是更多)。

TDD对于生成高质量的代码而非过度工程的要求非常宝贵,并且确保您具有100%的功能覆盖率(而不是线路覆盖率,这通常是没有意义的)。它需要改变您考虑编码的方式,这就是为什么在测试的同时学习该技术很有价值的原因。一旦你得到它,它将变得自然。

下一步是研究模拟策略:)

进行有趣的测试。