Introduction: Why is testing needed in Java?
\nEvery Java developer has at least once encountered a situation where changing a single line of code "broke" the entire system. To avoid such disasters, the industry has developed a coherent testing system. In this article, we will cover three levels of testing in Java: unit, integration, and e2e (end-to-end). You will learn how they differ, what tools to use, and how to write tests in Java correctly.
\nWhether you are writing microservices on Spring Boot or desktop applications, understanding the testing pyramid is an essential skill. Let's go!
\n\n1. Unit Testing: The Foundation of Quality
\nUnit tests verify the operation of individual classes or methods in isolation from external dependencies. This is the fastest and cheapest level of testing.
\n\n1.1 Main Tools
\n- \n
- JUnit 5 — the standard framework for Java (latest version is Jupiter). \n
- Mockito — a library for creating mock objects (stubs). \n
- AssertJ — a library with a rich set of fluent assertions. \n
1.2 Example of a Unit Test with JUnit 5 and Mockito
\nSuppose we have a service UserService that depends on a repository UserRepository:
public class UserService {\n private final UserRepository repository;\n\n public UserService(UserRepository repository) {\n this.repository = repository;\n }\n\n public String getUserName(Long id) {\n User user = repository.findById(id);\n return user != null ? user.getName() : "Unknown";\n }\n}\nLet's write a unit test, isolating the external dependency via Mockito:
\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass UserServiceTest {\n\n @Mock\n private UserRepository userRepository;\n\n @InjectMocks\n private UserService userService;\n\n @Test\n void testGetUserName_WhenUserExists() {\n // Arrange\n User mockUser = new User(1L, "Alice");\n when(userRepository.findById(1L)).thenReturn(mockUser);\n\n // Act\n String result = userService.getUserName(1L);\n\n // Assert\n assertEquals("Alice", result);\n }\n\n @Test\n void testGetUserName_WhenUserNotFound() {\n when(userRepository.findById(99L)).thenReturn(null);\n\n String result = userService.getUserName(99L);\n\n assertEquals("Unknown", result);\n }\n}\nBest practice: one test — one assertion. Use clear method names (e.g., testGetUserName_WhenUserExists).
\n\n2. Integration Testing: Verifying Interactions
\nIntegration tests verify how system components work together: database, file system, external APIs. They are slower than unit tests but provide more confidence in the correctness of the connections.
\n\n2.1 Tools for Integration Testing in Java
\n- \n
- Spring Boot Test —
@SpringBootTestfor loading the application context. \n - Testcontainers — running real databases (PostgreSQL, MySQL) in Docker containers. \n
- H2 — in-memory database for fast tests. \n
2.2 Example of an Integration Test with Spring Boot and Testcontainers
\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.t