所以我读过官方的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大师的见解!
答案 0 :(得分:17)
有趣的问题。首先 - 我在IDE中配置的终极测试模式:
@Test
public void shouldDoSomethingWhenSomeEventOccurs() throws Exception
{
//given
//when
//then
}
我总是从这个代码开始(聪明人称之为BDD)。
在given
中,我为每个测试设置了唯一的测试设置。
when
理想情况下是一行 - 您正在测试的内容。
then
应包含断言。
我不是单个断言提倡者,但是应仅测试行为的单个方面。例如,如果该方法应返回某些内容并且还有一些副作用,请创建两个具有相同given
和when
部分的测试。
此外,测试模式还包括throws Exception
。这是为了处理Java中烦人的检查异常。如果你测试一些抛出它们的代码,你就不会受到编译器的困扰。当然,如果测试抛出异常就会失败。
测试设置非常重要。一方面,提取公共代码并将其放在setup()
/ @Before
方法中是合理的。但请注意,在阅读测试时(且可读性是单元测试中的最大值!),很容易错过在测试用例开始时悬挂的设置代码。因此,相关的测试设置(例如,您可以以不同的方式创建窗口小部件)应该转到测试方法,但应该提取基础设施(设置常见的模拟,启动嵌入式测试数据库等)。再一次提高可读性。
您是否也知道JUnit会为每个测试创建测试用例类的新实例?因此,即使您在构造函数中创建了CUT( test in ),也会在每次测试之前调用构造函数。有点烦人。
首先命名您的测试,并考虑您要测试的用例或功能,从不考虑以下方面:
这是一个
Foo
类,有bar()
和buzz()
方法,因此我使用FooTest
和testBar()
创建testBuzz()
。哦,亲爱的,我需要在bar()
中测试两条执行路径 - 所以让我们创建testBar1()
和testBar2()
。
shouldTurnOffEngineWhenOutOfFuel()
很好,testEngine17()
很糟糕。
testGetBuzzWhenFooIsNullAndFizzIsNonNegative
名称告诉测试的内容是什么?我知道测试的东西,但为什么呢?你不觉得细节太贴心吗?怎么样:
@Test shouldReturnDisabledBuzzWhenFooNotProvidedAndFizzNotNegative`
它以有意义的方式描述输入和您的意图(假设禁用的buzz 是某种buzz
状态/类型)。另请注意,我们不再对getBuzz()
的{{1}}方法名称和null
合同进行硬编码(我们会说:未提供Foo
时)。如果您将来用 null对象模式替换Foo
该怎么办?
另外,不要害怕 20种null
的不同测试方法。请考虑您正在测试的20个不同的用例。但是,如果您的测试用例类增长得太大(因为它通常比测试类大得多),请提取几个测试用例。再一次:getBuzz()
,FooHappyPathTest
和FooBogusInput
都很好,FooCornerCases
和Foo1Test
都不好。
争取简短的描述性名称。 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%的功能覆盖率(而不是线路覆盖率,这通常是没有意义的)。它需要改变您考虑编码的方式,这就是为什么在测试的同时学习该技术很有价值的原因。一旦你得到它,它将变得自然。下一步是研究模拟策略:)
进行有趣的测试。