Unit Testing in Java: JUnit 5 and Mockito for Beginners
Codevity Admin · 10/25/2026
Step 31 of 38 · Java Interview Preparation for Beginners · Stage 6: Databases, Spring and testing
Codevity Admin · 10/25/2026
Testers are not the only people who write tests. In most Java teams, every pull request is expected to include unit tests, and interviewers ask freshers how they would test a method. This article gives you the vocabulary and the two libraries almost everyone uses: JUnit 5 (the test framework) and Mockito (for replacing dependencies).
A unit test checks one small piece of behaviour (a method or class) in isolation, quickly (milliseconds), with no network, database or file system, and gives the same answer every time. It follows Arrange, Act, Assert:
public class MiniTest {
static int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("divisor must not be zero");
return a / b;
}
public static void main(String[] args) {
int result = divide(10, 2); // Arrange and Act
System.out.println(result == 5); // true (Assert)
try {
divide(1, 0);
System.out.println("no exception was thrown");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // divisor must not be zero
}
}
}A test framework does the same thing, but reports each check separately, runs every test even when one fails, and plugs into your build tool (mvn test, gradle test).
// Illustrative: needs JUnit 5 (org.junit.jupiter:junit-jupiter) on the classpath.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator(); // a fresh object for every test
}
@Test
@DisplayName("adds two numbers")
void add() {
assertEquals(5, calculator.add(2, 3));
}
@Test
void divideByZeroIsRejected() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> calculator.divide(1, 0));
assertEquals("divisor must not be zero", e.getMessage());
}
@ParameterizedTest
@CsvSource({"10, 2, 5", "9, 3, 3", "-8, 2, -4"})
void divide(int a, int b, int expected) {
assertEquals(expected, calculator.divide(a, b));
}
}| Annotation / method | Purpose |
|---|---|
@Test | Marks a test method |
@BeforeEach / @AfterEach | Run around every test |
@BeforeAll / @AfterAll | Run once per class (static methods) |
@ParameterizedTest | One test, many inputs |
@Disabled | Skip a test (temporarily, with a reason) |
assertEquals(expected, actual) | Note the order: expected first |
assertTrue, assertNull, assertThrows, assertAll | Other checks. assertAll reports every failure at once |
JUnit 4 versus 5: org.junit.Test became org.junit.jupiter.api.Test, @Before became @BeforeEach, @RunWith became @ExtendWith, and tests no longer need to be public.
| Double | What it is |
|---|---|
| Dummy | Passed in but never used (to satisfy a constructor) |
| Stub | Returns canned answers |
| Fake | A simple working version (such as an in-memory repository or the FakeSender in the DI article) |
| Mock | Records how it was called so you can verify interactions |
| Spy | A real object whose behaviour is partly recorded or overridden |
// Illustrative: needs JUnit 5 and Mockito (org.mockito:mockito-junit-jupiter) on the classpath.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class EmployeeServiceTest {
@Mock
EmployeeRepository repository; // the database is replaced by a mock
@InjectMocks
EmployeeService service; // the mock is injected through the constructor
@Test
void findReturnsTheStoredEmployee() {
when(repository.findById(1L)).thenReturn(Optional.of(new Employee("Asha", "IT")));
Optional<Employee> found = service.find(1L);
assertTrue(found.isPresent());
assertEquals("Asha", found.get().name());
verify(repository).findById(1L); // the repository was asked exactly once
}
@Test
void findReturnsEmptyForUnknownId() {
when(repository.findById(99L)).thenReturn(Optional.empty());
assertTrue(service.find(99L).isEmpty());
verify(repository, never()).save(any()); // nothing was saved
}
}Mockito stubs with when(...).thenReturn(...) (or thenThrow), verifies with verify(...) and matches arguments with any(), eq(...) and argThat(...). Over-mocking produces tests that only restate the implementation. Mock the boundaries (database, HTTP client, clock), and use real objects for simple collaborators.
Write many fast unit tests, fewer integration tests (several real components, such as the service and a real database) and very few slow end-to-end tests. Test behaviour that matters: the happy path, edge cases (, empty, boundary values) and failure paths.
@SpringBootTest@WebMvcTest(EmployeeController.class)MockMvc@MockBean@MockitoBean@DataJpaTestnullCalculatorLocalDate.now()java.time.Clock