Skip to main content
Advertisement

18.1 Getting Started with JUnit 5

JUnit 5 is the most widely used unit testing framework in the Java ecosystem. Well-written tests are evidence of quality code. They catch bugs early and make refactoring safe.

1. Why Write Tests?

  • Early bug detection: Verify that changes don't break existing functionality
  • Confident refactoring: If tests pass, you can freely improve internal implementation
  • Living documentation: Well-written tests explain how code is supposed to be used
  • Better design: Code that's hard to test usually has design problems

2. Setup

// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test { useJUnitPlatform() }

3. Your First Test

// Calculator.java
public class Calculator {
public int add(int a, int b) { return a + b; }
public int subtract(int a, int b) { return a - b; }
public int multiply(int a, int b) { return a * b; }
public double divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Cannot divide by zero.");
return (double) a / b;
}
}
// CalculatorTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
Calculator calculator;

@BeforeEach
void setUp() { calculator = new Calculator(); }

@Test
@DisplayName("Addition: 3 + 5 = 8")
void testAdd() {
assertEquals(8, calculator.add(3, 5), "Addition result is incorrect");
}

@Test
@DisplayName("Division: 10 / 4 = 2.5")
void testDivide() {
assertEquals(2.5, calculator.divide(10, 4), 0.001);
}

@Test
@DisplayName("Division by zero throws exception")
void testDivideByZero() {
assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0));
}
}

4. Key Annotations

AnnotationDescription
@TestMarks a test method
@DisplayNameHuman-readable test name
@BeforeEachRuns before each test (setup)
@AfterEachRuns after each test (cleanup)
@BeforeAllRuns once before all tests (must be static)
@AfterAllRuns once after all tests (must be static)
@DisabledSkip this test
@NestedNested test class

5. Assertions

import static org.junit.jupiter.api.Assertions.*;

@Test
void assertionsDemo() {
assertEquals(3, 1 + 2);
assertNotEquals(5, 1 + 2);
assertTrue(5 > 3);
assertFalse(3 > 5);
assertNull(null);
assertNotNull("hello");

assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});

// Exception assertion
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> { throw new IllegalArgumentException("test error"); }
);
assertEquals("test error", ex.getMessage());

// Timeout assertion
assertTimeout(java.time.Duration.ofMillis(100), () -> Thread.sleep(50));

// Multiple assertions (all run even if one fails)
assertAll("user validation",
() -> assertEquals("Alice", "Alice"),
() -> assertTrue(30 >= 18),
() -> assertNotNull("email@example.com")
);
}

6. Parameterized Tests

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedTestDemo {

@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
@DisplayName("Positive number check")
void testPositive(int number) { assertTrue(number > 0); }

@ParameterizedTest
@CsvSource({"3, 5, 8", "10, -3, 7", "0, 0, 0", "-1, -1, -2"})
@DisplayName("Addition multiple cases")
void testAddMultiple(int a, int b, int expected) {
assertEquals(expected, new Calculator().add(a, b));
}

@ParameterizedTest
@MethodSource("provideStrings")
@DisplayName("Blank string check")
void testIsBlank(String input, boolean expected) {
assertEquals(expected, input.isBlank());
}

static java.util.stream.Stream<Arguments> provideStrings() {
return java.util.stream.Stream.of(
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("hello", false)
);
}
}

7. @Nested - Hierarchical Test Structure

@DisplayName("User service tests")
class UserServiceTest {
UserService userService;

@BeforeEach void setUp() { userService = new UserService(); }

@Nested @DisplayName("Registration")
class SignUp {
@Test @DisplayName("Success with valid data") void success() { /* ... */ }
@Test @DisplayName("Fail with duplicate email") void dupEmail() { /* ... */ }
@Test @DisplayName("Fail with short password") void shortPwd() { /* ... */ }
}

@Nested @DisplayName("Login")
class Login {
@Test @DisplayName("Success with valid credentials") void success() { /* ... */ }
@Test @DisplayName("Fail with wrong password") void wrongPwd() { /* ... */ }
}
}

Pro Tip

Good test principles (FIRST):

  • Fast: Tests should run quickly (minimize DB, network)
  • Independent: No test should depend on another
  • Repeatable: Same result anywhere, anytime
  • Self-validating: Pass/fail automatically (no manual checking)
  • Timely: Write tests right after (or before — TDD) writing code

Coverage goals: 100% is not the goal. Focus on core business logic and edge cases. Simple getters/constructors have low test value.

Advertisement