Writing unit tests that only check happy paths leaves your code half-examined. When methods throw exceptions, those failure paths deserve coverage too. The developers behind codingnconcepts.com walk through exception assertion strategies using both JUnit 5 and AssertJ, revealing how each library tackles the challenge with its own philosophy.
At a glance
- JUnit 5 supplies assertThrows() and assertDoesNotThrow() for exception verification
- AssertJ provides fluent chaining with assertThatThrownBy() and related methods
- Both libraries extract caught exceptions for message and cause validation
- AssertJ includes prebuilt shortcuts for frequent exception types
- Exception tests document expected failure behavior alongside success cases
Why Exception Coverage Deserves Attention
Most developers write tests that verify correct outputs. Far fewer test what happens when inputs go wrong. A NullPointerException reaching production because nobody wrote a test for it? That scenario plays out in codebases everywhere.
The codingnconcepts.com guide stresses that all expected exceptions should appear in your test suite. Beyond catching bugs early, exception tests serve as living documentation. They show future maintainers exactly how the code should behave when something breaks.
Our analysis: JUnit 5 and AssertJ solve the same problem with different philosophies. JUnit 5 uses a direct, assert-then-capture approach. AssertJ chains assertions fluently, making tests more readable at the cost of an extra dependency. For teams already using AssertJ elsewhere, consistency argues for staying with it. The critical point is simply having the tests.
JUnit 5's Approach
JUnit 5 gives you two core assertions. Use assertThrows() when you expect an exception to occur. Use assertDoesNotThrow() when a method should complete cleanly.
The guide works through a DataProcessor class with a process() method. That method throws when it receives invalid input. A test exercises the method with bad data, captures the result, then inspects it:
Throwable caught = assertThrows(RuntimeException.class, () -> processor.process(null));
The guide shows extracting and validating the exception message and cause afterward. This pattern applies to any exception type. Want to test IndexOutOfBoundsException instead? Replace the class name and the lambda. Want to verify the exact message text? Add an assertion on getMessage().
AssertJ's Fluent Assertions
AssertJ takes a different angle. Rather than naming the exception upfront, you describe the call, then chain verifications. The style reads almost like English sentences.
For instance, you might check that code throws an exception, verify its type, confirm the message, and inspect the underlying cause:
AssertThatThrownBy(() -> processor.process(null))
.isInstanceOf(RuntimeException.class)
.hasMessage("Processing failed")
.hasCauseInstanceOf(NullPointerException.class);
Three main entry points exist. AssertThatThrownBy() catches any exception. AssertThatExceptionOfType() specifies the type first. AssertThatRuntimeException() works as a shortcut when you know the exception class. All three let you chain further assertions, so pick whichever makes your test read most naturally.
One nice touch: AssertJ ships with specialized assertions for common exceptions. Methods like assertThatIndexOutOfBoundsException() and assertThatIllegalArgumentException() reduce boilerplate and signal intent instantly.
Putting It Into Practice
Android developers work with Java every day, so these patterns apply directly. Suppose your app has a utility that parses user input. You want to confirm that passing an empty string triggers an IllegalArgumentException.
With JUnit 5:
AssertThrows(IllegalArgumentException.class, () -> inputParser.sanitize(""));
With AssertJ:
AssertThatIllegalArgumentException().isThrownBy(() -> inputParser.sanitize(""))
.withMessage("Cannot process empty input");
Both capture the failure scenario. Neither requires digging through crash logs later.
Bottom Line
Exception tests are a hallmark of mature test suites. The techniques from codingnconcepts.com work in any Java project. Whether you favor JUnit 5's straightforward style or AssertJ's chained syntax, start covering those edge cases. Every expected exception you test is one less bug making it to production.
Key takeaways
- Write exception tests alongside happy-path coverage to catch edge case failures early
- JUnit 5's assertThrows() handles simple cases well; AssertJ's chaining improves readability for complex checks
- Verify exception messages and cause chains to make tests more meaningful
- Use assertDoesNotThrow() or assertThatNoException() to confirm methods that should never fail
- AssertJ's shortcut assertions for common types reduce code and clarify intent
Frequently asked questions
Do I need to add both JUnit 5 and AssertJ to my project?
No, either library works independently. Many teams standardize on one for consistency across all assertions.
How do I confirm a method does not throw an exception?
JUnit 5 provides assertDoesNotThrow() while AssertJ offers assertThatNoException() for this verification.
Can I check the exception message alongside the type?
Yes. After capturing the exception, both libraries let you assert on getMessage() or chain .hasMessage() for more readable syntax.
What are AssertJ shortcut assertions?
Prebuilt methods like assertThatNullPointerException(), assertThatIllegalArgumentException(), and assertThatIndexOutOfBoundsException() target frequent exception types with less setup.
Does this apply to Android unit tests?
Absolutely. Android projects use Java or Kotlin, so these same techniques work directly when testing Android components with JUnit and AssertJ.