Testing in Java: From Unit to End-to-End Tests
Quality testing is the foundation of reliable software. In the Java world, there is an established testing pyramid that includes three main levels: unit, integration, and e2e (end-to-end) tests. In this article, we will break down each of them, provide practical examples in Java, and explain how to properly build a testing strategy in your project.
1. Unit Testing: The Foundation of the Pyramid
Unit tests check individual components of a program in isolation — typically one class or method. They run quickly, do not require external dependencies (DB, API), and should cover most business logic.
Tools for Unit Tests in Java
- JUnit 5 — standard framework for writing tests.
- Mockito — library for creating mock objects.
- AssertJ — fluent assertions for readable checks.
Example of a Unit Test with JUnit 5 and Mockito
Suppose we have an OrderService that depends on PaymentGateway. We want to test the discount calculation logic without calling the real payment gateway.
// Class under testpublic class OrderService { private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; }
public double calculateDiscount(double amount) { if (amount > 1000) { return amount * 0.1; // 10% discount } return 0; }}
// 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.assertEquals;
@ExtendWith(MockitoExtension.class)public class OrderServiceTest {
@Mock private PaymentGateway paymentGateway;
@InjectMocks private OrderService orderService;
@Test public void testCalculateDiscount_AmountAbove1000_Returns10Percent() { double result = orderService.calculateDiscount(1500); assertEquals(150.0, result, 0.001); }
@Test public void testCalculateDiscount_AmountBelow1000_ReturnsZero() { double result = orderService.calculateDiscount(500); assertEquals(0.0, result); }}Best practices: use @BeforeEach to initialize common data, avoid testing private methods directly, follow the "one assertion per test" rule.
2. Integration Testing: Checking Interaction
Integration tests check how multiple modules work together — for example, a service interacting with a database, message queues, or external APIs. Unlike unit tests, these use real or in-memory dependencies.
Tools for Integration Tests
- Spring Boot Test — for testing Spring applications.
- Testcontainers — for spinning up Docker containers (DB, Kafka).
- H2 Database — in-memory database for fast tests.
Example of an Integration Test with Spring Boot
Testing a REST controller with a real database connection (using H2 in the test).
// Controller@RestController@RequestMapping("/api/users")public class UserController { private final UserRepository userRepository;
public UserController(UserRepository userRepository) { this.userRepository = userRepository; }
@GetMapping("/{id}") public User getUser(@PathVariable Long id) { return userRepository.findById(id) .orElseThrow(() -> new Runti