Unit testing with JUnit 5 in Java
@Test, assertions, assertThrows, lifecycle annotations, parameterized tests.
A test is a method that can fail
JUnit runs every method marked **@Test and reports which ones fail. Inside, assertions check results. The most common: assertEquals(expected, actual)**.
@Test
void addsTwoNumbers() {
var calc = new Calc();
assertEquals(5, calc.add(2, 3));
}Expected comes first
assertEquals takes the expected value first and the actual value second. The test passes either way, but on failure JUnit prints *expected: <5> but was: <4>*. Swapped arguments make that message lie to whoever reads it.
Testing exceptions
**assertThrows(Type.class, lambda) runs the lambda, checks that it throws that type, and returns the exception** so you can inspect it further.
var ex = assertThrows(
ArithmeticException.class,
() -> calc.divide(1, 0));
assertEquals("/ by zero", ex.getMessage());Throwing outside the lambda
Here withdraw(-5) runs before assertThrows, so its exception escapes and the test errors. The call that should throw belongs inside the lambda: () -> acc.withdraw(-5).
@Test
void rejectsNegativeAmount() {
Account acc = new Account();
acc.withdraw(-5); // throws here!
assertThrows(IllegalArgumentException.class,
() -> acc.balance());
}The lifecycle
By default JUnit 5 creates a new instance of the test class for each test method, so fields can't leak between tests. **@BeforeEach/@AfterEach run around every test. @BeforeAll/@AfterAll run once per class, so they're static by default: there's no instance yet. @Disabled("reason")** skips a test.
Shared counter?
A test class has a field int count = 0;. Two tests each do count++ and then assertEquals(1, count). What happens?
Both passThe second one failsIt depends on the order
Show the answer
Both pass. Each test method gets a fresh instance of the class, so each starts with count = 0. That independence is the point.
Many inputs, one test
@Test void two() { assertEquals(0, 2 % 2); }
@Test void four() { assertEquals(0, 4 % 2); }Every new case means another copied method.
@ParameterizedTest
@ValueSource(ints = {2, 4, 6})
void isEven(int n) {
assertEquals(0, n % 2);
}One test, run once per value. @CsvSource takes strings like "1, 2, 3" for several arguments.
Tests in the pipeline
Every push to CI runs the whole suite. Fast, independent tests are what make that bearable: a failure points at one method, not at "something in the build". A shared static field that leaks state between tests is the classic source of flaky, order-dependent failures.
Key takeaways
- assertEquals(expected, actual): order matters for messages
- assertThrows runs a lambda and returns the exception
- A new test class instance is created per test method
- @ParameterizedTest + @ValueSource / @CsvSource
JUnit 5 is really three parts: the JUnit Platform (launches tests), JUnit Jupiter (the new programming model) and JUnit Vintage (runs old JUnit 3 and 4 tests).
Practice questions
In which order does assertEquals take its arguments?
- assertEquals(actual, expected)
- assertEquals(expected, actual)
- Order doesn't exist; it takes a single boolean
- assertEquals(message, actual)
Check your answer
assertEquals(expected, actual). JUnit expects the expected value first and the actual value second.
Complete the annotation so the test runs once for each number.
@ParameterizedTest
@___(ints = {2, 4, 6})
void isEven(int n) {
assertEquals(0, n % 2);
}- ValueSource
- BeforeEach
- Test
- CsvSource
Check your answer
ValueSource. @ValueSource supplies a single literal value per invocation, here through ints = {...}. @CsvSource uses strings of comma-separated values, not an ints attribute.