Unit Testing
By the end of this lesson you'll be able to write JUnit 5 tests that prove your code works — using assertions, lifecycle hooks, parameterized cases, and Mockito mocks — so you can refactor without fear and ship code teams trust.
Learn Unit Testing in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
Before You Start
You should be comfortable with OOP (classes and interfaces, which Mockito mocks) and Annotations (JUnit is annotation-driven). A look at Exception Handling helps for the assertThrows parts.
🧪 A Real-World Analogy
💡 Analogy: A unit test is a smoke alarm for one room of your code. You set it up once, and from then on it watches that room silently. The day you change the wiring (refactor) and something starts to smoulder (a bug), the alarm goes off immediately — long before the fire reaches a user. A good test suite is a house full of alarms: cheap, automatic, and the reason you can sleep at night after deploying.
Manually re-checking your whole app after every change is like walking every room with a candle. Tests do that walk for you, in milliseconds, every time — and they never forget a room.
1️⃣ The Anatomy of a JUnit 5 Test
A unit test is a small method that runs one slice of your code and checks the result is what you expect. In JUnit 5 you mark such a method with @Test , and inside it you call an assertion — a check that fails the test if it isn't true.
- assertEquals(expected, actual) — fails unless the two are equal. Expected comes first.
- assertThrows(Exception.class, () -> code()) — passes only if that code throws.
- assertAll(...) — runs several checks and reports all failures, not just the first.
2️⃣ Lifecycle Hooks & Arrange–Act–Assert
Tests must be independent — running test B should never depend on test A having run first. Lifecycle hooks give each test a clean slate:
Hook
When it runs
Typical use
@BeforeEach
Before every @Test
Build fresh objects
@AfterEach
After every @Test
Clean up, reset state
@BeforeAll
Once, before all (static)
Expensive one-time setup
@AfterAll
Once, after all (static)
Shut down shared resources
Inside each test, follow Arrange → Act → Assert :
1. Arrange: set up the data and objects (often in @BeforeEach).
3️⃣ Mockito — Testing a Class in Isolation
Real classes have dependencies — a UserService needs a UserRepository that talks to a database. You don't want a real database in a unit test: it's slow and flaky. A mock is a fake stand-in you control.
- mock / @Mock — create a fake of an interface or class.
- when(mock.call()).thenReturn(value) — program ("stub") what the fake returns.
- verify(mock).call() — assert the fake was actually used as expected.
Mark the class under test with @InjectMocks and Mockito wires the mocks into it for you.
🎯 Your Turn #1 — Parameterized tests & assertAll
Fill in the three ___ blanks below to make the suite pass. You'll wire up @ValueSource , @CsvSource , and assertAll . The expected result is in the last comment.
🎯 Your Turn #2 — Stub and verify a mock
Finish this Mockito test: stub the fake PriceApi with when().thenReturn() , then verify() it was consulted. Two blanks, expected output in the comment.
🧩 Mini-Challenge — Test a BankAccount (no scaffold)
This time only the plan is given — you write the test methods. Use a @BeforeEach , an assertEquals , an assertThrows , and a @CsvSource case. Follow the numbered comments.
Common Errors & Fixes
- ❌ Testing implementation, not behaviour: asserting that a private helper was called ties the test to how the code works, so every harmless refactor breaks it. Fix: assert the observable outcome (the return value, the thrown exception), not the internal steps.
- ❌ No isolation between tests: reusing one shared object across tests lets test A leave data that makes test B pass or fail at random. Fix: rebuild state in @BeforeEach so every test starts clean.
- ❌ Over-mocking: mocking your own logic or value objects means the test only checks the mock setup, not real correctness — it passes even when the code is wrong. Fix: mock only external dependencies (DB, API, clock); use real objects for plain logic.
- ❌ assertEquals arguments backwards: assertEquals(actual, expected) still passes/fails correctly but prints a misleading "expected: <x> but was: <y>". Fix: always put the known-correct value first : assertEquals(expected, actual) .
- ❌ Comparing doubles without a delta: assertEquals(0.3, a + b) can fail on rounding. Fix: add a tolerance: assertEquals(0.3, a + b, 0.001) .
Pro Tips
- 💡 @DisplayName turns cryptic method names into readable report lines: @DisplayName("rejects a blank email") .
- 💡 @ParameterizedTest covers twenty edge cases in one method — and reports exactly which input failed.
- 💡 assertAll shows every broken expectation in one run, saving fix-rerun-fix cycles.
- 💡 Red → Green → Refactor: write a failing test, write the minimum code to pass, then clean up while the test stays green.
📋 Quick Reference
Annotation / Method
Purpose
Example
@Test
Mark a test method
@Test void works() { ... }
Setup before each test
cart = new Cart();
@DisplayName
Readable test name
@DisplayName("starts empty")
@ParameterizedTest
Run with many inputs
with @ValueSource / @CsvSource
assertEquals
Check equality
assertEquals(expected, actual)
assertThrows
Check it throws
assertThrows(Ex.class, () -> ...)
assertAll
Group assertions
assertAll(() -> ..., () -> ...)
@Mock / when
Fake + stub a dependency
when(m.x()).thenReturn(v)
verify
Check mock was called
verify(m).x()
Frequently Asked Questions
🎉 Lesson Complete!
You can now write professional unit tests: @Test methods with assertEquals , assertThrows , and assertAll ; clean state via @BeforeEach / @AfterEach / @BeforeAll ; broad coverage with @ParameterizedTest ; clear names with @DisplayName ; and isolation with Mockito's mock / when / verify — all structured as arrange–act–assert.
Next up: Maven & Gradle — the build tools that pull in JUnit and Mockito and run your whole test suite with one command.
Practice quiz
What is the difference between JUnit and Mockito?
- They are two names for the same library
- Mockito runs the tests; JUnit makes mocks
- JUnit is the test framework that runs tests; Mockito creates fake versions of dependencies
- JUnit only works with Mockito
Answer: JUnit is the test framework that runs tests; Mockito creates fake versions of dependencies. JUnit finds @Test methods, runs them, and reports pass/fail. Mockito fakes a class's collaborators (DB, API) so you can test in isolation. They are used together.
What is the correct argument order for assertEquals?
- assertEquals(expected, actual)
- assertEquals(actual, expected)
- The order does not matter at all
- assertEquals(message, expected)
Answer: assertEquals(expected, actual). The expected (known-correct) value goes first, the value your code produced goes second. Reversing it flips the failure message labels and misleads debugging.
How do you test that a method throws an exception in JUnit 5?
- try { code(); } catch (Exception e) { pass(); }
- assertEquals(Exception.class, code())
- @Test(expected = Exception.class) only
- assertThrows(SomeException.class, () -> code())
Answer: assertThrows(SomeException.class, () -> code()). assertThrows(SomeException.class, () -> code()) passes when that exception type (or a subclass) is thrown, and returns the caught exception for further checks.
What does @BeforeEach do?
- Runs once before all tests in the class
- Runs before every single @Test method, giving each a clean starting state
- Runs after each test
- Marks a test method
Answer: Runs before every single @Test method, giving each a clean starting state. @BeforeEach runs before every @Test so each test gets fresh state and cannot leak data into the next. @BeforeAll runs only once and must be static.
Why use @ParameterizedTest instead of many separate @Test methods?
- It writes the test once and feeds many inputs, reporting each case separately
- It runs tests faster on the GPU
- It hides failing cases
- It is required for every test
Answer: It writes the test once and feeds many inputs, reporting each case separately. @ParameterizedTest with @ValueSource or @CsvSource avoids copy-pasting, reports exactly which input failed, and makes adding a case one line.
What does Mockito's when(mock.call()).thenReturn(value) do?
- Verifies the mock was called
- Creates a real object
- Stubs the fake so it returns the given value for that call
- Throws an exception
Answer: Stubs the fake so it returns the given value for that call. when(...).thenReturn(...) programs (stubs) what the fake returns. verify(mock).call() is the separate step that asserts the call actually happened.
When should you use a mock instead of a real object?
- For your own plain logic and value objects
- For slow, external, or non-deterministic dependencies like databases and HTTP APIs
- For everything, always
- Never use mocks
Answer: For slow, external, or non-deterministic dependencies like databases and HTTP APIs. Mock only slow/external/non-deterministic collaborators. Over-mocking your own logic means the test only checks the mock wiring, not real correctness.
Why does comparing doubles in assertEquals often need a delta, e.g. assertEquals(1059.97, total, 0.001)?
- Deltas make tests run faster
- It is required for all assertEquals calls
- The delta is the expected value
- Doubles are inexact, so an exact comparison can fail on rounding
Answer: Doubles are inexact, so an exact comparison can fail on rounding. Floating-point arithmetic is inexact, so a + b might be 0.30000000000000004. A small tolerance (delta) makes the comparison robust.
What does assertAll do?
- Stops at the first failing assertion
- Evaluates every grouped assertion and reports all failures at once
- Runs all test classes
- Asserts a mock was called
Answer: Evaluates every grouped assertion and reports all failures at once. assertAll runs each grouped lambda assertion and reports every failure in one run, saving fix-rerun-fix cycles.
What does @InjectMocks do in a Mockito test?
- Creates a fake of an interface
- Runs the test in parallel
- Injects the created @Mock dependencies into the class under test
- Verifies all mocks were used
Answer: Injects the created @Mock dependencies into the class under test. @InjectMocks marks the real class under test and wires the @Mock fakes into it automatically, so you test that class against controlled dependencies.
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson