By WebNest Studio Engineering Team
Advanced Java Tutorial
Unit Testing with JUnit
A unit test verifies that a single, small piece of code — typically one method — behaves correctly in isolation. JUnit is the dominant testing framework in the Java ecosystem, and JUnit 5 (also called JUnit Jupiter) is its current generation, built around annotations that mark methods as tests and assertions that check expected outcomes.
Tests matter because they turn "I think this still works" into "I know this still works." Without automated tests, verifying that a change hasn't broken existing behavior means manually re-checking the application by hand, which does not scale and is easy to skip under deadline pressure. A solid test suite catches regressions the moment they're introduced, often before the code is even committed.
Why it matters
Code that has no tests can be verified only by running the application and clicking through it. Unit tests check behaviour in milliseconds on every change, which is what makes refactoring safe. JUnit is the standard in Java, and writing tests is part of the job in practically every team.
Core JUnit 5 Annotations
JUnit 5 tests are ordinary methods marked with annotations that tell the test runner how and when to invoke them.
@Test— marks a method as a test case to be executed by the JUnit runner.@BeforeEach— runs before every test method in the class, commonly used to set up fresh test fixtures so tests don't leak state between each other.@AfterEach— runs after every test method, often used for cleanup.@BeforeAll/@AfterAll— run once before/after all tests in the class (must be static methods).@DisplayName— gives a test a readable, human-friendly name in test reports.
Assertions
Assertions, from the org.junit.jupiter.api.Assertions class, are what actually determine pass or fail. assertEquals(expected, actual) checks two values match. assertTrue / assertFalse check a boolean condition. assertThrows(ExceptionType.class, () -> { ... }) verifies that a block of code throws a specific exception — essential for testing error-handling paths, not just happy paths. If an assertion fails, JUnit stops that test method and reports it as a failure with a clear expected-vs-actual message.
The Arrange-Act-Assert Pattern
Well-structured unit tests follow a consistent three-part shape. Arrange: set up the objects and inputs needed for the test. Act: call the method under test. Assert: check that the outcome matches expectations. Keeping these three steps visually separate (often with blank lines) makes tests easy to read at a glance, even for someone unfamiliar with the codebase, and keeps each test focused on verifying one specific behavior.
Example
A JUnit 5 test class covering a normal case and an exception case
// src/main/java/com/webnest/math/Calculator.java
package com.webnest.math;
public class Calculator {
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return a / b;
}
}
// src/test/java/com/webnest/math/CalculatorTest.java
package com.webnest.math;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
// Arrange (shared setup): a fresh Calculator before every test
calculator = new Calculator();
}
@Test
void dividesTwoPositiveNumbers() {
// Act
int result = calculator.divide(10, 2);
// Assert
assertEquals(5, result);
}
@Test
void throwsWhenDividingByZero() {
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
}
CalculatorTest
✔ dividesTwoPositiveNumbers()
✔ throwsWhenDividingByZero()
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
Common Mistakes
- Writing a test that depends on the outcome or side effects of another test running first, making the suite fragile and order-dependent.
- Testing only the "happy path" and never verifying that invalid input or edge cases correctly throw exceptions or return error results.
- Naming tests vaguely (test1, testMethod) instead of describing the behavior being verified, making failures hard to interpret.
- Asserting too many unrelated things in a single test method, so a failure doesn't clearly point to what actually broke.
Exercise
This exercise needs JUnit 5 on the classpath, through Maven or Gradle, so it does not run in the playground. For the Calculator class given, write a test class with two tests: one checking that 10 divided by 2 is 5, and one checking that dividing by zero throws ArithmeticException. Create a fresh calculator before each test.
Expected result: the build tool reports two tests passed.
class Calculator {
int divide(int a, int b) {
return a / b;
}
}
// CalculatorTest.java
// TODO: imports from org.junit.jupiter.api
class CalculatorTest {
// TODO: a Calculator field, initialised before each test
// TODO: test that divide(10, 2) returns 5
// TODO: test that divide(1, 0) throws ArithmeticException
}
Show hints
- A method annotated with
@BeforeEachruns before every test method. assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0))passes only if that exception is thrown.
Show solution
class Calculator {
int divide(int a, int b) {
return a / b;
}
}
// CalculatorTest.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
void dividesTwoNumbers() {
assertEquals(5, calculator.divide(10, 2));
}
@Test
void throwsWhenDividingByZero() {
assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0));
}
}
Quick quiz
1. In assertEquals(a, b), which argument comes first?
- A. The actual value
- B. The message
- C. The tolerance
- D. The expected value
Show answer
D. Getting the order right makes the failure message read correctly.
2. When does a method annotated with @BeforeEach run?
- A. Once before all the tests
- B. Before every test method
- C. After every test method
- D. Only when a test fails
Show answer
B. @BeforeAll is the one that runs once for the whole class.
3. Which assertion checks that code throws a particular exception?
- A. assertThrows
- B. assertFails
- C. assertError
- D. expectException
Show answer
A. It also returns the exception, so its message can be checked.
Interview questions
What makes a good unit test?
Show answer
It tests one behaviour and has a name that states it. It is fast, so that thousands can run on every change. It is independent of other tests and of their order, and deterministic, giving the same result every time, with no dependence on the network, the clock or a database. It follows a clear arrange, act, assert structure, and it fails for one reason only, so a failure points directly at the cause.
What is the difference between JUnit 4 and JUnit 5?
Show answer
JUnit 5 is modular, made of the Platform, the Jupiter API and the Vintage engine for old tests. Its annotations were renamed: @BeforeEach and @AfterEach replace @Before and @After, and @BeforeAll replaces @BeforeClass. It adds assertThrows, nested and parameterised tests, display names, and extensions in place of runners and rules. Test classes and methods no longer need to be public.
Key Points to Remember
- @Test marks a test method; @BeforeEach/@AfterEach set up and tear down fixtures around each test.
- assertEquals, assertTrue, and assertThrows are core assertions for verifying expected outcomes, including expected exceptions.
- The Arrange-Act-Assert pattern keeps tests readable and focused on one behavior at a time.
- A solid test suite catches regressions automatically, which is far more reliable than manual re-checking.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering