Testing in Java: Unit, Integration, E2E — A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Testing in Java: from Unit to End-to-End Tests



Code quality is one of the main indicators of a developer's professionalism. In the Java world, testing has long become not just a good practice, but an industry standard. Without reliable tests, it's difficult to maintain large projects, introduce new features, and confidently refactor code. In this article, we will break down three key levels of testing: unit, integration, and e2e (end-to-end). You will learn how they differ, how to write them correctly in Java, and which tools to use.



The Test Pyramid and Why It Matters

Before diving into the details, it's worth recalling the concept of the test pyramid (Mike Cohn). It visualizes the ideal ratio of tests at different levels:

  • Unit tests (base of the pyramid) — the fastest, cheapest, and most numerous. They should make up about 70%.
  • Integration tests (middle) — check component interaction. They make up about 20%.
  • E2E tests (top) — the slowest and most expensive, but critically important for verifying user scenarios. They make up about 10%.

Adhering to this proportion allows you to gain maximum confidence in the code with minimal time spent on test execution.



1. Unit Testing in Java

Unit tests check a single "unit" of code in isolation — typically a method or class. Their main goal is to ensure that the method's logic works correctly, independent of external dependencies (databases, APIs, file systems). Mock objects are used for isolation.



Main Tools

  • JUnit 5 — the standard framework for writing tests.
  • Mockito — a library for creating mock objects and verifying interactions.
  • AssertJ — a library for fluent assertions (improves readability).


Example of a Unit Test with JUnit 5 and Mockito

Suppose we have a service UserService that depends on UserRepository:

public class UserService {    private final UserRepository repository;        public UserService(UserRepository repository) {        this.repository = repository;    }        public User findById(Long id) {        return repository.findById(id)                .orElseThrow(() -> new UserNotFoundException("User not found"));    }}

Test for this method:

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;import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)class UserServiceTest {

@Mock private UserRepository repository;

@InjectMocks private UserService userService;

@Test void testFindById_UserExists() { // Given Long userId = 1L; User expectedUser = new User(userId, "Alice"); when(repository.findById(userId)).thenReturn(Optional.of(expectedUser));

// When User actualUser = userService.findById(userId);

// Then assertNotNull(actualUser); assertEquals("Alice", actualUser.getName()); verify(repository).findById(userId); }

@Test void testFindById_UserNotFound() { // Given Long userId = 99L; when(repository.findById(userId)).thenReturn(Optional.empty());

// When & Then assertThrows(UserNotFoundException.class, () -> userService.findById(userId)); }}

Best practices:

  • Test one

Blogs

Book Recommendations