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.

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.

💡 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.

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.

Tests must be independent — running test B should never depend on test A having run first. Lifecycle hooks give each test a clean slate:

Inside each test, follow Arrange → Act → Assert :

1. Arrange: set up the data and objects (often in @BeforeEach).

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.

Mark the class under test with @InjectMocks and Mockito wires the mocks into it for you.

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.

Finish this Mockito test: stub the fake PriceApi with when().thenReturn() , then verify() it was consulted. Two blanks, expected output in the comment.

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.

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.