Spring Testing

1. Unit Testing

JUnit 5 và Mockito

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void whenFindByUsername_thenReturnUser() {
        // Given
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");

        when(userRepository.findByUsername("testuser"))
            .thenReturn(Optional.of(user));

        // When
        User found = userService.findByUsername("testuser");

        // Then
        assertNotNull(found);
        assertEquals("testuser", found.getUsername());
        assertEquals("test@example.com", found.getEmail());

        verify(userRepository).findByUsername("testuser");
    }

    @Test
    void whenFindByUsername_thenThrowException() {
        // Given
        when(userRepository.findByUsername("nonexistent"))
            .thenReturn(Optional.empty());

        // When & Then
        assertThrows(UserNotFoundException.class, () -> {
            userService.findByUsername("nonexistent");
        });
    }
}

Service Layer Testing

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void whenCreateOrder_thenSuccess() {
        // Given
        OrderRequest request = new OrderRequest();
        request.setProductId(1L);
        request.setQuantity(2);

        Order order = new Order();
        order.setId(1L);
        order.setStatus(OrderStatus.CREATED);

        when(orderRepository.save(any(Order.class)))
            .thenReturn(order);
        when(paymentService.processPayment(any(PaymentRequest.class)))
            .thenReturn(true);

        // When
        OrderResponse response = orderService.createOrder(request);

        // Then
        assertNotNull(response);
        assertEquals(OrderStatus.CREATED, response.getStatus());

        verify(orderRepository).save(any(Order.class));
        verify(paymentService).processPayment(any(PaymentRequest.class));
    }

    @Test
    void whenPaymentFails_thenThrowException() {
        // Given
        OrderRequest request = new OrderRequest();
        when(paymentService.processPayment(any(PaymentRequest.class)))
            .thenReturn(false);

        // When & Then
        assertThrows(PaymentFailedException.class, () -> {
            orderService.createOrder(request);
        });
    }
}

2. Integration Testing

@SpringBootTest

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setup() {
        userRepository.deleteAll();
    }

    @Test
    void whenCreateUser_thenStatus201() throws Exception {
        UserDto userDto = new UserDto();
        userDto.setUsername("newuser");
        userDto.setEmail("new@example.com");

        mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(userDto)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.username").value("newuser"))
            .andExpect(jsonPath("$.email").value("new@example.com"));

        assertTrue(userRepository.findByUsername("newuser").isPresent());
    }

    @Test
    void whenGetNonExistentUser_thenStatus404() throws Exception {
        mockMvc.perform(get("/api/users/{id}", 999))
            .andExpect(status().isNotFound());
    }
}

TestContainers

@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void registerPgProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void whenSaveUser_thenSuccess() {
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");

        User saved = userRepository.save(user);

        assertNotNull(saved.getId());
        assertEquals("testuser", saved.getUsername());
    }
}

3. API Testing

REST Assured

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApiIntegrationTest {

    @LocalServerPort
    private int port;

    private String baseUrl;

    @BeforeEach
    void setup() {
        baseUrl = "http://localhost:" + port + "/api";
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }

    @Test
    void whenGetUsers_thenSuccess() {
        given()
            .contentType(ContentType.JSON)
        .when()
            .get(baseUrl + "/users")
        .then()
            .statusCode(200)
            .body("size()", greaterThan(0))
            .body("[0].username", notNullValue());
    }

    @Test
    void whenCreateInvalidUser_thenBadRequest() {
        UserDto invalidUser = new UserDto();
        // Missing required fields

        given()
            .contentType(ContentType.JSON)
            .body(invalidUser)
        .when()
            .post(baseUrl + "/users")
        .then()
            .statusCode(400)
            .body("message", containsString("validation failed"));
    }
}

4. Security Testing

WebMvcTest với Security

@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @MockBean
    private JwtTokenProvider tokenProvider;

    @Test
    @WithMockUser(roles = "ADMIN")
    void whenAdminAccessSecuredEndpoint_thenSuccess() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "USER")
    void whenUserAccessAdminEndpoint_thenForbidden() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
    }

    @Test
    void whenNoAuth_thenUnauthorized() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isUnauthorized());
    }
}

5. Best Practices

1. Test Configuration

@TestConfiguration
public class TestConfig {

    @Bean
    public TestRestTemplate testRestTemplate() {
        return new TestRestTemplate();
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper;
    }
}

2. Test Data Factory

public class TestDataFactory {

    public static User createTestUser() {
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("password123");
        user.setRoles(Set.of(Role.ROLE_USER));
        return user;
    }

    public static Order createTestOrder(User user) {
        Order order = new Order();
        order.setUser(user);
        order.setStatus(OrderStatus.CREATED);
        order.setTotalAmount(BigDecimal.valueOf(100.00));
        return order;
    }
}

3. Custom Annotations

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@Transactional
public @interface TransactionalTest {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@WithMockUser
public @interface AuthenticatedTest {
}

4. Test Utilities

public class TestUtils {

    public static String asJsonString(Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T fromJsonString(String json, Class<T> clazz) {
        try {
            return new ObjectMapper().readValue(json, clazz);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void clearDatabase(JdbcTemplate jdbcTemplate) {
        List<String> tableNames = jdbcTemplate.queryForList(
            "SELECT table_name FROM information_schema.tables " +
            "WHERE table_schema='public'",
            String.class
        );

        tableNames.forEach(tableName ->
            jdbcTemplate.execute("TRUNCATE TABLE " + tableName + " CASCADE")
        );
    }
}

5. Performance Testing

```java @SpringBootTest public class PerformanceTest {

@Autowired
private UserService userService;

@Test
void testBulkOperationPerformance() {
    int numberOfUsers = 1000;
    List<User> users = IntStream.range(0, numberOfUsers)
        .mapToObj(i -> {
            User user = new User();
            user.setUsername("user" + i);
            user.setEmail("user" + i + "@example.com");
            return user;
        })
        .collect(Collectors.toList());

    long startTime = System.currentTimeMillis();

    userService.saveAll(users);

    long endTime = System.currentTimeMillis();
    long duration = endTime - startTime;

    assertTrue(duration < 5000, // 5 seconds
        "Bulk operation took too long: " + duration + "ms");
}

}