How do you assert that a certain exception is thrown in JUnit 4 tests?

前端 未结 30 1938
忘掉有多难
忘掉有多难 2020-11-21 22:23

How can I use JUnit4 idiomatically to test that some code throws an exception?

While I can certainly do something like this:

@Test
public void testFo         


        
相关标签:
30条回答
  • 2020-11-21 23:08

    As answered before, there are many ways of dealing with exceptions in JUnit. But with Java 8 there is another one: using Lambda Expressions. With Lambda Expressions we can achieve a syntax like this:

    @Test
    public void verifiesTypeAndMessage() {
        assertThrown(new DummyService()::someMethod)
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Runtime exception occurred")
                .hasMessageStartingWith("Runtime")
                .hasMessageEndingWith("occurred")
                .hasMessageContaining("exception")
                .hasNoCause();
    }
    

    assertThrown accepts a functional interface, whose instances can be created with lambda expressions, method references, or constructor references. assertThrown accepting that interface will expect and be ready to handle an exception.

    This is relatively simple yet powerful technique.

    Have a look at this blog post describing this technique: http://blog.codeleak.pl/2014/07/junit-testing-exception-with-java-8-and-lambda-expressions.html

    The source code can be found here: https://github.com/kolorobot/unit-testing-demo/tree/master/src/test/java/com/github/kolorobot/exceptions/java8

    Disclosure: I am the author of the blog and the project.

    0 讨论(0)
  • 2020-11-21 23:08

    Additionally to what NamShubWriter has said, make sure that:

    • The ExpectedException instance is public (Related Question)
    • The ExpectedException isn't instantiated in say, the @Before method. This post clearly explains all the intricacies of JUnit's order of execution.

    Do not do this:

    @Rule    
    public ExpectedException expectedException;
    
    @Before
    public void setup()
    {
        expectedException = ExpectedException.none();
    }
    

    Finally, this blog post clearly illustrates how to assert that a certain exception is thrown.

    0 讨论(0)
  • 2020-11-21 23:12

    JUnit has built-in support for this, with an "expected" attribute.

    0 讨论(0)
  • 2020-11-21 23:12

    We can use an assertion fail after the method that must return an exception:

    try{
       methodThatThrowMyException();
       Assert.fail("MyException is not thrown !");
    } catch (final Exception exception) {
       // Verify if the thrown exception is instance of MyException, otherwise throws an assert failure
       assertTrue(exception instanceof MyException, "An exception other than MyException is thrown !");
       // In case of verifying the error message
       MyException myException = (MyException) exception;
       assertEquals("EXPECTED ERROR MESSAGE", myException.getMessage());
    }
    
    0 讨论(0)
  • 2020-11-21 23:13

    tl;dr

    • post-JDK8 : Use AssertJ or custom lambdas to assert exceptional behaviour.

    • pre-JDK8 : I will recommend the old good try-catch block. (Don't forget to add a fail() assertion before the catch block)

    Regardless of Junit 4 or JUnit 5.

    the long story

    It is possible to write yourself a do it yourself try-catch block or use the JUnit tools (@Test(expected = ...) or the @Rule ExpectedException JUnit rule feature).

    But these ways are not so elegant and don't mix well readability wise with other tools. Moreover, JUnit tooling does have some pitfalls.

    1. The try-catch block you have to write the block around the tested behavior and write the assertion in the catch block, that may be fine but many find that this style interrupts the reading flow of a test. Also, you need to write an Assert.fail at the end of the try block. Otherwise, the test may miss one side of the assertions; PMD, findbugs or Sonar will spot such issues.

    2. The @Test(expected = ...) feature is interesting as you can write less code and then writing this test is supposedly less prone to coding errors. But this approach is lacking in some areas.

      • If the test needs to check additional things on the exception like the cause or the message (good exception messages are really important, having a precise exception type may not be enough).
      • Also as the expectation is placed around in the method, depending on how the tested code is written then the wrong part of the test code can throw the exception, leading to false-positive test and I'm not sure that PMD, findbugs or Sonar will give hints on such code.

        @Test(expected = WantedException.class)
        public void call2_should_throw_a_WantedException__not_call1() {
            // init tested
            tested.call1(); // may throw a WantedException
        
            // call to be actually tested
            tested.call2(); // the call that is supposed to raise an exception
        }
        
    3. The ExpectedException rule is also an attempt to fix the previous caveats, but it feels a bit awkward to use as it uses an expectation style, EasyMock users know very well this style. It might be convenient for some, but if you follow Behaviour Driven Development (BDD) or Arrange Act Assert (AAA) principles the ExpectedException rule won't fit in those writing style. Aside from that it may suffer from the same issue as the @Test way, depending on where you place the expectation.

      @Rule ExpectedException thrown = ExpectedException.none()
      
      @Test
      public void call2_should_throw_a_WantedException__not_call1() {
          // expectations
          thrown.expect(WantedException.class);
          thrown.expectMessage("boom");
      
          // init tested
          tested.call1(); // may throw a WantedException
      
          // call to be actually tested
          tested.call2(); // the call that is supposed to raise an exception
      }
      

      Even the expected exception is placed before the test statement, it breaks your reading flow if the tests follow BDD or AAA.

      Also, see this comment issue on JUnit of the author of ExpectedException. JUnit 4.13-beta-2 even deprecates this mechanism:

      Pull request #1519: Deprecate ExpectedException

      The method Assert.assertThrows provides a nicer way for verifying exceptions. In addition, the use of ExpectedException is error-prone when used with other rules like TestWatcher because the order of rules is important in that case.

    So these above options have all their load of caveats, and clearly not immune to coder errors.

    1. There's a project I became aware of after creating this answer that looks promising, it's catch-exception.

      As the description of the project says, it let a coder write in a fluent line of code catching the exception and offer this exception for the latter assertion. And you can use any assertion library like Hamcrest or AssertJ.

      A rapid example taken from the home page :

      // given: an empty list
      List myList = new ArrayList();
      
      // when: we try to get the first element of the list
      when(myList).get(1);
      
      // then: we expect an IndexOutOfBoundsException
      then(caughtException())
              .isInstanceOf(IndexOutOfBoundsException.class)
              .hasMessage("Index: 1, Size: 0") 
              .hasNoCause();
      

      As you can see the code is really straightforward, you catch the exception on a specific line, the then API is an alias that will use AssertJ APIs (similar to using assertThat(ex).hasNoCause()...). At some point the project relied on FEST-Assert the ancestor of AssertJ. EDIT: It seems the project is brewing a Java 8 Lambdas support.

      Currently, this library has two shortcomings :

      • At the time of this writing, it is noteworthy to say this library is based on Mockito 1.x as it creates a mock of the tested object behind the scene. As Mockito is still not updated this library cannot work with final classes or final methods. And even if it was based on Mockito 2 in the current version, this would require to declare a global mock maker (inline-mock-maker), something that may not what you want, as this mock maker has different drawbacks that the regular mock maker.

      • It requires yet another test dependency.

      These issues won't apply once the library supports lambdas. However, the functionality will be duplicated by the AssertJ toolset.

      Taking all into account if you don't want to use the catch-exception tool, I will recommend the old good way of the try-catch block, at least up to the JDK7. And for JDK 8 users you might prefer to use AssertJ as it offers may more than just asserting exceptions.

    2. With the JDK8, lambdas enter the test scene, and they have proved to be an interesting way to assert exceptional behaviour. AssertJ has been updated to provide a nice fluent API to assert exceptional behaviour.

      And a sample test with AssertJ :

      @Test
      public void test_exception_approach_1() {
          ...
          assertThatExceptionOfType(IOException.class)
                  .isThrownBy(() -> someBadIOOperation())
                  .withMessage("boom!"); 
      }
      
      @Test
      public void test_exception_approach_2() {
          ...
          assertThatThrownBy(() -> someBadIOOperation())
                  .isInstanceOf(Exception.class)
                  .hasMessageContaining("boom");
      }
      
      @Test
      public void test_exception_approach_3() {
          ...
          // when
          Throwable thrown = catchThrowable(() -> someBadIOOperation());
      
          // then
          assertThat(thrown).isInstanceOf(Exception.class)
                            .hasMessageContaining("boom");
      }
      
    3. With a near-complete rewrite of JUnit 5, assertions have been improved a bit, they may prove interesting as an out of the box way to assert properly exception. But really the assertion API is still a bit poor, there's nothing outside assertThrows.

      @Test
      @DisplayName("throws EmptyStackException when peeked")
      void throwsExceptionWhenPeeked() {
          Throwable t = assertThrows(EmptyStackException.class, () -> stack.peek());
      
          Assertions.assertEquals("...", t.getMessage());
      }
      

      As you noticed assertEquals is still returning void, and as such doesn't allow chaining assertions like AssertJ.

      Also if you remember name clash with Matcher or Assert, be prepared to meet the same clash with Assertions.

    I'd like to conclude that today (2017-03-03) AssertJ's ease of use, discoverable API, the rapid pace of development and as a de facto test dependency is the best solution with JDK8 regardless of the test framework (JUnit or not), prior JDKs should instead rely on try-catch blocks even if they feel clunky.

    This answer has been copied from another question that don't have the same visibility, I am the same author.

    0 讨论(0)
  • 2020-11-21 23:14

    Using an AssertJ assertion, which can be used alongside JUnit:

    import static org.assertj.core.api.Assertions.*;
    
    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
      Foo foo = new Foo();
    
      assertThatThrownBy(() -> foo.doStuff())
            .isInstanceOf(IndexOutOfBoundsException.class);
    }
    

    It's better than @Test(expected=IndexOutOfBoundsException.class) because it guarantees the expected line in the test threw the exception and lets you check more details about the exception, such as message, easier:

    assertThatThrownBy(() ->
           {
             throw new Exception("boom!");
           })
        .isInstanceOf(Exception.class)
        .hasMessageContaining("boom");
    

    Maven/Gradle instructions here.

    0 讨论(0)
提交回复
热议问题