JUnit Testing in Spring Boot (User Management System)
Learn testing systematically: unit -> web -> repository -> edge cases -> interview readiness.
@Mock, controller tests with @WebMvcTest + @MockBean + MockMvc, repository tests with @DataJpaTest, and focused exception/edge-case tests. This gives fast feedback plus production confidence.Definition
JUnit is the core Java testing framework; Mockito provides test doubles; Spring Boot test slices let you test each layer in isolation without loading unnecessary context.
Why It Matters
- Prevents regressions during refactoring and feature additions.
- Builds confidence for CI/CD pipelines and safer deployments.
- Interviewers evaluate your ability to test design, not only write business code.
Code Variations
pom.xml test dependencyxml<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>@Mock + @InjectMocks)java@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@InjectMocks UserService userService;
@Test
void createUser_savesAndReturnsUser() {
User user = new User(1L, "John Doe", "john@example.com");
when(userRepository.save(any(User.class))).thenReturn(user);
User saved = userService.createUser(user);
assertEquals("John Doe", saved.getName());
verify(userRepository).save(user);
}
}@WebMvcTest + MockMvc)java@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void getUserByEmail_returns200() throws Exception {
User user = new User(1L, "Alice", "alice@example.com");
when(userService.getUserByEmail("alice@example.com"))
.thenReturn(Optional.of(user));
mockMvc.perform(get("/users/alice@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
}@DataJpaTest)java@DataJpaTest
class UserRepositoryTest {
@Autowired TestEntityManager em;
@Autowired UserRepository userRepository;
@Test
void findByEmail_userExists_returnsUser() {
em.persist(new User(null, "John", "john@example.com"));
em.flush();
Optional<User> found = userRepository.findByEmail("john@example.com");
assertTrue(found.isPresent());
}
}flush() forces SQL execution before query assertions.@Test
void createUser_duplicateEmail_throwsConflict() {
User req = new User(null, "John", "john@example.com");
when(userRepository.findByEmail("john@example.com"))
.thenReturn(Optional.of(new User(1L, "John", "john@example.com")));
assertThrows(DuplicateEmailException.class, () -> userService.createUser(req));
}Which test type for which layer?
| Aspect | Unit Test | Web Slice Test | Repository Integration Test |
|---|---|---|---|
| Annotation | @ExtendWith(MockitoExtension.class) | @WebMvcTest | @DataJpaTest |
| Scope | Single class logic | Controller + MVC config | JPA repositories + DB |
| Dependencies | Mock all collaborators | @MockBean service/deps | Real JPA + test DB |
| Speed | Fastest | Fast | Moderate |
| Best for | Business rules | HTTP contract | Query correctness |
Common Confusions Cleared Up
@Mock vs @MockBean@Mock is pure Mockito (no Spring context). @MockBean registers/replaces a bean inside Spring test context (e.g., in @WebMvcTest, @SpringBootTest).
@WebMvcTest vs @SpringBootTest@WebMvcTest is focused and fast for controller contracts. @SpringBootTest loads full app context and is used when you need broader integration.
persist() vs merge() in JPA testspersist() is for new entities; merge() reattaches detached entities and returns managed instance.
Variations / Implementations
Step 1: Introduction (JUnit + Mockito)
JUnit structures tests (@Test, assertions), while Mockito isolates dependencies by stubbing behavior (when(...).thenReturn(...)). In layered Spring apps this is essential for true unit tests.
Step 2: Setup in Spring Boot
Use spring-boot-starter-test. It already includes JUnit 5, Mockito, AssertJ, JSONassert, and Spring test utilities. Avoid mixing JUnit 4 unless required by legacy code.
Step 3: Service unit tests
Test business logic with @ExtendWith(MockitoExtension.class), @Mock, and @InjectMocks. No real DB/network should be involved.
Step 4: Controller tests
Use @WebMvcTest + MockMvc + @MockBean to validate request mapping, status codes, payloads, and error responses without starting full server.
Step 5: Repository integration tests
Use @DataJpaTest for JPA repositories. Persist test data with TestEntityManager, call repository methods, and assert DB behavior.
Step 6: Exceptions and edge cases
Validate duplicate email, invalid input, not-found, DB error paths, and API contract (400/404/409/500) so failure handling is intentional.
Step 7: Best practices + interview prep
Keep tests deterministic, independent, and readable. Prefer behavior-focused names and verify interactions only when meaningful.
Common Mistakes / Anti-Patterns
- Using
@SpringBootTestfor every test (slow suite). - Mocking too much in repository tests (defeats integration purpose).
- Asserting implementation details instead of observable behavior.
- Tests dependent on execution order/shared mutable state.
- No negative-path tests (only happy path).
Interview Q&A
Likely Follow-up Questions
- How would you test async methods and scheduled jobs?
- How to test security (
@WithMockUser, JWT filters)? - When would you use Testcontainers over H2?
- How to keep test data setup maintainable at scale?
Quick Cheat Sheet
@Mock + @InjectMocks + behavior assertions.@WebMvcTest + MockMvc + @MockBean.@DataJpaTest + persist/flush + query assertions.