Testing in Java: unit, integration, e2e from A to Z

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

Introduction: Why testing is the foundation of quality Java code

Every Java developer has encountered a situation where fixing one bug creates three new ones. To break this vicious cycle, there is testing. It is not just a checkbox on a checklist, but a professional discipline that saves nerves, money, and the product's reputation.

In this article, we will break down three key levels of testing: unit, integration, and e2e (end-to-end). We will look at them with practical Java examples, using popular frameworks: JUnit 5, Mockito, Spring Boot Test, and Selenium. You will learn how to build the right testing pyramid and not overload your project with useless tests.



1. Unit testing: Checking atomic units of code

Unit tests are the lowest and fastest level. They check the work of a single "unit" (usually a method or class) in isolation from external dependencies (DB, API, file system).



1.1 Tools and frameworks

  • JUnit 5 — the de facto standard for writing tests in Java.
  • Mockito — a library for creating mocks and stubs.
  • AssertJ — a library for fluent assertions.


1.2 Example of a Unit test with JUnit 5 and Mockito

Let's imagine we have an OrderService that depends on a PaymentRepository. We want to test the service logic without calling the real database.

// Class under testpublic class OrderService {    private final PaymentRepository paymentRepository;

public OrderService(PaymentRepository paymentRepository) { this.paymentRepository = paymentRepository; }

public String processOrder(Order order) { if (order.getAmount() <= 0) { throw new IllegalArgumentException("Order amount must be positive"); } boolean success = paymentRepository.pay(order); return success ? "Payment successful" : "Payment error"; }}

// Unit testimport 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 static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)class OrderServiceTest {

@Mock private PaymentRepository paymentRepository;

@InjectMocks private OrderService orderService;

@Test void testProcessOrder_Success() { // Arrange Order order = new Order(100.0); when(paymentRepository.pay(order)).thenReturn(true);

// Act String result = orderService.processOrder(order);

// Assert assertEquals("Payment successful", result); verify(paymentRepository, times(1)).pay(order); }

@Test void testProcessOrder_NegativeAmount() { // Arrange Order order = new Order(-10.0);

// Act & Assert assertThrows(IllegalArgumentException.class, () -> { orderService.processOrder(order); }); }}

Best Practices for Unit tests:

  • One test — one assertion (FIRST principle: Fast, Isolated, Repeatable, Self-validating, Timely).
  • Use clear names: shouldReturnSuccess_whenPaymentIsApproved.
  • Do not test private methods — test the public contract.
  • Cover edge cases: null, empty strings, negative numbers.


2. Integration testing: Checking component interaction

Integration tests check how different modules of the system

Blogs

Book Recommendations