Phase 5 · Professional DevelopmentModule 29~40 min read

Testing with JUnit

Prove your code works with automated tests, JUnit 5, mocking, and test-driven development.

What you'll learn

How do you know your code works — and keeps working after every change? You write automated tests. This module introduces JUnit 5, the standard testing framework for Java, and the habits that separate hobby code from professional software.

By the end you'll be able to:

  • Explain why automated testing matters and the levels of testing
  • Write and run tests with JUnit 5 using the Arrange-Act-Assert pattern
  • Use assertions, lifecycle methods, and exception tests
  • Run one test over many inputs with parameterized tests
  • Isolate code under test with mocking (Mockito)
  • Understand test-driven development (TDD)

Why test?

Manual testing ("run it and see") doesn't scale and doesn't catch regressions — bugs a new change reintroduces into old code. Automated tests run in seconds, document how your code should behave, and give you the confidence to refactor. Tests come in levels, and a healthy suite has far more fast unit tests than slow end-to-end ones:

The testing pyramid
System / E2EFew — slow, whole app
IntegrationSome — modules together
UnitMany — fast, one class

Your first test

A JUnit test is a method annotated with @Test. Structure it with the Arrange-Act-Assert pattern for clarity — set things up, do the thing, then check the result:

Arrange · Act · Assert

Arrange

Set up the objects and inputs

→

Act

Call the method under test

→

Assert

Check the result is what you expect

CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class Calculator {
    int add(int a, int b) { return a + b; }
}

class CalculatorTest {
    @Test
    void addsTwoNumbers() {
        Calculator calc = new Calculator();   // Arrange
        int result = calc.add(2, 3);           // Act
        assertEquals(5, result);               // Assert
    }
}
Your IDE or build tool runs tests and reports pass/fail.

Assertions

An assertion is the check that decides pass or fail — if it's false, the test fails with a helpful message. JUnit's Assertions class has one for every situation:

Assertions.java
assertEquals(5, calc.add(2, 3));      // equal?
assertTrue(calc.add(2, 3) > 0);       // condition true?
assertFalse("hi".isBlank());          // condition false?
assertNull(cache.get("missing"));     // is null?
assertNotNull(user.getName());        // is not null?
assertThrows(ArithmeticException.class,
    () -> { int x = 1 / 0; });         // does it throw?

Tip

Note assertThrows takes a lambda — it runs the code and passes only if the expected exception is thrown. Prefer specific assertions (assertEquals) over generic ones (assertTrue(a == b)): the failure messages are far more useful.

Lifecycle & exceptions

Tests should be independent — each starting from a clean slate. Lifecycle annotations run setup and cleanup around your tests: @BeforeEach/@AfterEach around every test, and @BeforeAll/@AfterAll once for the whole class:

Lifecycle.java
class OrderServiceTest {

    OrderService service;

    @BeforeEach                 // runs before EVERY test
    void setUp() {
        service = new OrderService();
    }

    @Test
    void placesOrder() { /* uses a fresh service */ }

    @AfterEach                  // runs after EVERY test (cleanup)
    void tearDown() { /* close resources */ }
}

Parameterized tests

When you want to test the same logic against many inputs, don't copy-paste the test — use a parameterized test. JUnit runs it once per value:

Parameterized.java
@ParameterizedTest
@ValueSource(ints = {2, 4, 6, 100, -8})
void isEven(int number) {
    assertEquals(0, number % 2);   // one test, run 5 times
}

Test doubles & mocking

A true unit test isolates one class. But classes have collaborators — a database, an API, a payment gateway — that are slow or have side effects. A mock is a fake stand-in you control: you tell it what to return and verify how it was used. Mockito is the go-to library:

Mocking.java
// Mockito creates a fake collaborator you fully control
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.charge(100)).thenReturn(true);

OrderService service = new OrderService(gateway);
boolean ok = service.checkout(100);

assertTrue(ok);
verify(gateway).charge(100);   // confirm it was called

Note

Mocks let you test the "unhappy paths" that are hard to trigger for real — what happens when the payment fails, the network times out, or the database is down.

Test-driven development

TDD flips the usual order into a short loop: Red (write a failing test for the behaviour you want) → Green (write the simplest code to pass it) → Refactor (clean up, tests still green). It keeps you focused, guarantees coverage, and produces code that's testable by design.

Recap & quick check

Key takeaways

  • Automated tests catch regressions, document behaviour, and enable fearless refactoring.
  • A JUnit test is an @Test method structured as Arrange-Act-Assert.
  • Assertions (assertEquals, assertThrows, …) decide pass/fail; prefer specific ones.
  • @BeforeEach/@AfterEach keep tests independent; @ParameterizedTest runs one test over many inputs.
  • Mock collaborators (Mockito) to isolate the unit; TDD = Red → Green → Refactor.

Quick check

1. What annotation marks a JUnit 5 test method?

2. What does the Arrange-Act-Assert pattern describe?

3. How do you verify that code throws an exception?

4. What is a mock used for?

5. What's the TDD cycle?

Great — you can now prove your code works. Next up: Module 30 — Debugging, Logging & Profiling.