简体   繁体   English

编写第一个JUnit测试

[英]Writing first JUnit test

So I've read the official JUnit docs, which contain a plethora of examples, but (as with many things) I have Eclipse fired up and I am writing my first JUnit test, and I'm choking on some basic design/conceptual issues. 所以我已经阅读了官方的JUnit文档,其中包含了大量的例子,但是(和许多事情一样)我已经启动了Eclipse,我正在编写我的第一个JUnit测试,并且我在讨论一些基本的设计/概念问题。

So if my WidgetUnitTest is testing a target called Widget , I assume I'll need to create a fair number of Widget s to use throughout the test methods. 因此,如果我的WidgetUnitTest正在测试一个名为Widget的目标,我假设我需要在整个测试方法中创建相当数量的Widget Should I be constructing these Widget s in the WidgetUnitTest constructor, or in the setUp() method? 我应该在WidgetUnitTest构造函数中还是在setUp()方法中构造这些Widget Should there be a 1:1 ratio of Widget s to test methods, or do best practices dictate reusing Widget s as much as possible? 如果Widget与测试方法的比例为1:1,或者最佳实践是否要求尽可能多地重用Widget

Finally, how much granularity should exist between asserts/fails and test methods? 最后,断言/失败和测试方法之间应该存在多少粒度? A purist might argue that 1-and-only-1 assertions should exist inside a test method , however under that paradigm, if Widget has a getter called getBuzz() , I'll end up with 20 different test methods for getBuzz() with names like 纯粹主义者可能认为测试方法中应该存在1对1的断言 ,但是在该范例下,如果Widget有一个名为getBuzz()的getter,我最终会得到20个不同的getBuzz()测试方法。名字喜欢

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

As opposed to 1 method that tests a multitude of scenarios and hosts a multitude of assertions: 与测试多种场景并承载大量断言的1种方法相反:

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

Thanks for any insight from some JUnit maestros! 感谢一些JUnit大师的见解!

Pattern 图案

Interesting question. 有趣的问题。 First of all - my ultimate test pattern configured in IDE: 首先 - 我在IDE中配置的终极测试模式:

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

    //when

    //then
}

I am always starting with this code (smart people call it BDD ). 我总是从这个代码开始(聪明的人称之为BDD )。

  • In given I place test setup unique for each test. given I测试设置中,每个测试都是唯一的。

  • when is ideally a single line - the thing you are testing. when理想上是一条线 - 你正在测试的东西。

  • then should contain assertions. then应该包含断言。

I am not a single assertion advocate, however you should test only single aspect of a behavior. 我不是一个单独的断言倡导者,但是你应该只测试一个行为的单个方面。 For instance if the the method should return something and also has some side effects, create two tests with same given and when sections. 例如,如果该方法应该返回的东西,也有一定的副作用,创建两个试验用同一given ,并when段。

Also the test pattern includes throws Exception . 测试模式还包括throws Exception This is to handle annoying checked exceptions in Java. 这是为了处理Java中烦人的检查异常。 If you test some code that throws them, you won't be bothered by the compiler. 如果你测试一些抛出它们的代码,你就不会受到编译器的困扰。 Of course if the test throws an exception it fails. 当然,如果测试抛出异常则失败。

Setup 建立

Test setup is very important. 测试设置非常重要。 On one hand it is reasonable to extract common code and place it in setup() / @Before method. 一方面,提取公共代码并将其放在setup() / @Before方法中是合理的。 However note that when reading a test ( and readability is the biggest value in unit testing! ) it is easy to miss setup code hanging somewhere at the beginning of the test case. 但请注意,在阅读测试时( 可读性是单元测试中的最大值! )很容易错过在测试用例开始时挂在某处的设置代码。 So relevant test setup (for instance you can create widget in different ways) should go to test method, but infrastructure (setting up common mocks, starting embedded test database, etc.) should be extracted. 因此,相关的测试设置(例如,您可以以不同的方式创建窗口小部件)应该转到测试方法,但应该提取基础设施(设置常见的模拟,启动嵌入式测试数据库等)。 Once again to improve readability. 再次提高可读性。

Also are you aware that JUnit creates new instance of test case class per each test? 您是否也知道JUnit每次测试都会创建测试用例类的新实例? So even if you create your CUT ( class under test ) in the constructor, the constructor is called before each test. 因此,即使您在构造函数中创建了CUT( 测试中的 ),也会在每次测试之前调用构造函数。 Kind of annoying. 有点烦人。

Granularity 粒度

First name your test and think what use-case or functionality you want to test, never think in terms of: 首先命名您的测试并考虑您要测试的用例或功能,从不考虑以下方面:

this is a Foo class having bar() and buzz() methods so I create FooTest with testBar() and testBuzz() . 这是一种Foo具有类bar()buzz()方法,以便创建FooTesttestBar()testBuzz() Oh dear, I need to test two execution paths throughout bar() - so let us create testBar1() and testBar2() . 哦,亲爱的,我需要在bar()测试两个执行路径 - 所以让我们创建testBar1()testBar2()

shouldTurnOffEngineWhenOutOfFuel() is good, testEngine17() is bad. shouldTurnOffEngineWhenOutOfFuel()是好的, testEngine17()是坏的。

More on naming 更多关于命名

What does the testGetBuzzWhenFooIsNullAndFizzIsNonNegative name tell about the test? testGetBuzzWhenFooIsNullAndFizzIsNonNegative名称告诉测试的是什么? I know it tests something, but why? 我知道它会测试一些东西,但为什么呢? And don't you think the details are too intimate? 你不觉得细节太贴心吗? How about: 怎么样:

@Test shouldReturnDisabledBuzzWhenFooNotProvidedAndFizzNotNegative`

It both describes the input in a meaningful manner and your intent (assuming disabled buzz is some sort of buzz status/type). 它以有意义的方式描述输入和您的意图(假设禁用的蜂鸣声是某种buzz状态/类型)。 Also note we no longer hardcode getBuzz() method name and null contract for Foo (instead we say: when Foo is not provided ). 另请注意,我们不再对Foo getBuzz()方法名称和null契约进行硬编码(相反,我们说: 当没有提供Foo )。 What if you replace null with null object pattern in the future? 如果将来用null对象模式替换null怎样?

Also don't be afraid of 20 different test methods for getBuzz() . 另外不要害怕getBuzz()20种不同的测试方法 Instead think of 20 different use cases you are testing. 请考虑您正在测试的20个不同的用例。 However if your test case class grows too big (since it is typically much larger than tested class), extract into several test cases. 但是,如果您的测试用例类增长得太大(因为它通常比测试类大得多),请提取几个测试用例。 Once again: FooHappyPathTest , FooBogusInput and FooCornerCases are good, Foo1Test and Foo2Test are bad. 再一次: FooHappyPathTestFooBogusInputFooCornerCases都很好, Foo1TestFoo2Test都很糟糕。

Readability 可读性

Strive for short and descriptive names. 争取简短和描述性的名字。 Few lines in given and few in then . given几行和then很少的行。 That's it. 而已。 Create builders and internal DSLs, extract methods, write custom matchers and assertions. 创建构建器和内部DSL,提取方法,编写自定义匹配器和断言。 The test should be even more readable than production code. 测试应该比生产代码更具可读性。 Don't over-mock. 不要过度嘲笑。

I find it useful to first write a series of empty well-named test case methods. 我发现首先编写一系列空的,命名良好的测试用例方法很有用。 Then I go back to the first one. 然后我回到第一个。 If I still understand what was I suppose to test under what conditions, I implement the test building a class API in the meantime. 如果我仍然明白我想在什么条件下测试什么,我在此期间实现构建类API的测试。 Then I implement that API. 然后我实现了那个API。 Smart people call it TDD (see below). 聪明人称之为TDD(见下文)。

Recommended reading: 推荐阅读:

You would create a new instance of the class under test in your setup method. 您将在setup方法中创建一个受测试类的新实例。 You want each test to be able to execute independently without having to worry about any unwanted state in the object under test from another previous test. 您希望每个测试能够独立执行,而不必担心另一个先前测试中被测对象中的任何不需要的状态。

I would recommend having separate test for each scenario/behavior/logic flow that you need to test, not one massive test for everything in getBuzz(). 我建议对需要测试的每个场景/行为/逻辑流进行单独测试,而不是对getBuzz()中的所有内容进行大规模测试。 You want each test to have a focused purpose of what you want to verify in getBuzz(). 您希望每个测试都具有您想要在getBuzz()中验证的内容。

Rather than testing methods try to focus on testing behaviors. 而不是测试方法尝试专注于测试行为。 Ask the question "what should a widget do?" 问一个问题“小部件应该做什么?” Then write a test that affirms the answer. 然后写一个肯定答案的测试。 Eg. 例如。 "A widget should fidget" “一个小部件应该坐立不安”

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

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

compile, see "no method fidget defined " errors, fix the errors, recompile the test and repeat. 编译,看“没有方法fidget定义”错误,修复错误,重新编译测试并重复。 Next ask the question what should the result of each behavior be, in our case what happens as the result of fidget? 接下来问问题每个行为的结果应该是什么,在我们的例子中,fidget的结果是什么? Maybe there is some observable output like a new 2D coordinate position. 也许有一些可观察的输出就像一个新的2D坐标位置。 In this case our widget would be assumed to be in a given position and when it fidgets it's position is altered some way. 在这种情况下,我们的小部件将被假定为处于给定位置,当它成为小部件时,它的位置会以某种方式改变。

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

Some would argue against both tests exercising the same fidget behavior but it makes sense to single out the behavior of fidget independent of how it impacts widget.position(). 有些人会反对两种测试运行相同的fidget行为,但是有必要单独指出fidget的行为,而不管它如何影响widget.position()。 If one behavior breaks the single test will pinpoint the cause of failure. 如果一个行为中断,单个测试将查明失败的原因。 Also it is important to state that the behavior can be exercised on its own as a fulfillment of the spec (you do have program specs don't you?) that suggests you need a fidgety widget. 此外,重要的是要声明行为可以单独执行作为规范的实现(你确实有程序规范吗?),这表明你需要一个烦躁的小部件。 In the end it's all about realizing your program specs as code that exercises your interfaces which demonstrate both that you've completed the spec and secondly how one interacts with your product. 最后,所有关于将程序规范实现为运行界面的代码,这些代码既展示了您已完成规范,又展示了如何与您的产品进行交互。 This is in essence how TDD should work. 这实际上是TDD应该如何工作的。 Any attempt to resolve bugs or test the product usually results in a frustrating pointless debate over which framework to use, level of coverage and how fine grained your suite should be. 任何解决错误或测试产品的尝试通常会导致对使用哪个框架,覆盖范围以及套件应该如何精细化的无意义争论。 Each test case should be an exercise of breaking down your spec into a component where you can begin phrasing with Given/When/Then. 每个测试用例都应该是将您的规范分解为一个组件,您可以使用Given / When / Then开始短语。 Given {some application state or precondition} When {a behavior is invoked} Then {assert some observable output}. 给定{某个应用程序状态或前提条件}当{调用行为}时{断言一些可观察的输出}。

First of all, the setUp and the tearDown Methods will be called before and after each Test, so the setUp Method should create the objects, if you need them in every test, and test-specific things may be done in the test itself. 首先,setUp和tearDown方法将在每个Test之前和之后调用,因此setUp Method应该创建对象,如果在每个测试中都需要它们,并且测试特定的事情可以在测试本身中完成。

Second, it is up to you how you want to test your program. 其次,由您决定如何测试您的程序。 Obviously you could write a test for every possible situation in your program and end up with a gazillion tests for every method. 显然,您可以为程序中的每种可能情况编写测试,并最终对每种方法进行大量测试。 Or you could write just one test for every method, which checks every possible scenario. 或者你可以为每个方法只编写一个测试,它会检查每个可能的场景。 I would recommend a mixture between both ways. 我会建议两种方式之间的混合。 You really don't need test for trivial getters/setters, but writing just one test for a method may result in confusion if the test fails. 你真的不需要测试琐碎的getter / setter,但是如果测试失败,只为一个方法编写一个测试可能会导致混淆。 You should decide, which Methods are worth testing, and which scenarios are worth testing. 您应该决定哪些方法值得测试,以及哪些方案值得测试。 But in principle every scenario should have its own Test. 但原则上每个场景都应该有自己的测试。

Mostly I end up with a code coverage of 80 to 90 percent with my tests. 通过我的测试,我最终得到了80%到90%的代码覆盖率。

I completely second Tomasz Nurkiewicz answer, so I'll say that rather than repeating everything he said. 我完全是第二个Tomasz Nurkiewicz的答案,所以我会说,而不是重复他说的一切。

A couple more points: 还有几点:

Don't forget to test error cases. 不要忘记测试错误情况。 You can consider something like that: 你可以考虑这样的事情:

@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());
    } 
}

Also, it has GREAT value to start writing your tests before even thinking about the design of your class under test. 此外,在考虑被测课程的设计之前,开始编写测试具有很大的价值。 If you're a beginner on unit-testing, I cannot emphasize enough learning this technique at the same time (this is called TDD, test-driven development) which proceeds like that: 如果你是单元测试的初学者,我不能强调同时学习这种技术(这被称为TDD,测试驱动开发),这样做:

  • You think about what user case you have for your user requirements 您可以考虑用户对用户要求的用例
  • You write a basic first test for it 你为它写了一个基本的第一个测试
  • You make it compile (by creating needed classes -including your class under test-, etc.) 你让它编译(通过创建所需的类 - 包括你的测试类 - 等)
  • You run it: it should fail 你运行它:它应该失败
  • Now you implement the functionality of the class under test that will make it pass (and nothing more ) 现在,您实现了测试类的功能,使其通过( 仅此而已
  • Rinse, and repeat with a new requirement 冲洗,并重复新的要求

When all your requirements have passing tests, then you're done. 当您的所有要求都通过测试时,您就完成了。 You NEVER write anything in your production code that doesn't have a test before (exceptions to that is logging code, and not much more). 你永远不要在你的生产代码中写任何没有测试的东西(例外情况是记录代码,而不是更多)。

TDD is invaluable in producing good quality code, not over-engineering requirements, and making sure you have a 100% functional coverage (rather than line coverage, which is usually meaningless). TDD对于生成高质量的代码而非过度工程要求非常宝贵,并且确保您具有100%的功能覆盖(而不是线覆盖,这通常是没有意义的)。 It requires a change in the way you consider coding, that's why it's valuable to learn the technique at the same time as testing. 它需要改变您考虑编码的方式,这就是为什么在测试的同时学习该技术很有价值的原因。 Once you get it, it will become natural. 一旦你得到它,它将变得自然。

Next step is looking into Mocking strategies :) 下一步是研究模拟策略:)

Have fun testing. 玩得开心测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM