Testes Automatizados - Backend FinBoost+
Visão Geral
O backend do FinBoost+ implementa uma estratégia robusta de testes automatizados usando JUnit 5, Mockito e Spring Boot Test. A arquitetura segue a pirâmide de testes, garantindo cobertura > 70% e qualidade de código.
Stack de Testes
Frameworks e Ferramentas
- JUnit 5: Framework principal de testes unitários
- Mockito: Framework de mocking para isolamento
- Spring Boot Test: Testes de integração e slice testing
- AssertJ: Assertions fluentes e expressivas
- JaCoCo: Cobertura de código
- H2: Banco em memória para testes
Dependências Principais
<dependencies>
<!-- Starter de testes (inclui JUnit 5, Mockito, AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Testes de segurança -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Arquitetura de Testes
Pirâmide de Testes
graph TD
A[Unit Tests - 70%] --> B[Slice Tests - 20%]
B --> C[Integration Tests - 10%]
A --> A1[Services isolados]
A --> A2[Lógica de negócio]
B --> B1[WebMvcTest]
B --> B2[DataJpaTest]
C --> C1[SpringBootTest]
C --> C2[Fluxos completos]
Categorias de Testes
- Unit Tests (70%): Testam classes isoladamente
- Slice Tests (20%): Testam fatias específicas da aplicação
- Integration Tests (10%): Testam fluxos completos
Estrutura de Testes
src/test/java/com/finboostplus/
├── config/ # Configurações de teste
│ ├── TestConfig.java # Beans para testes
│ └── TestSecurityConfig.java # Configuração de segurança
├── factory/ # Factories de dados de teste
│ ├── UserTestFactory.java # Criação de usuários
│ ├── GroupTestFactory.java # Criação de grupos
│ └── ExpenseTestFactory.java # Criação de despesas
├── controller/ # Testes de Controller
├── service/ # Testes de Service
├── repository/ # Testes de Repository
├── integration/ # Testes de Integração
└── util/ # Utilitários de teste
Tipos de Testes
1. Testes Unitários (Service Layer)
Características:
- Testam lógica de negócio isolada
- Usam mocks para dependências
- Execução rápida e independente
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
@DisplayName("Deve criar usuário com dados válidos")
void shouldCreateUserWithValidData() {
// Arrange
var createDTO = UserTestFactory.createValidUserDTO();
var encodedPassword = "encoded_password";
var savedUser = UserTestFactory.createUserEntity(createDTO);
savedUser.setId(1L);
when(userRepository.existsByEmail(createDTO.email())).thenReturn(false);
when(passwordEncoder.encode(createDTO.password())).thenReturn(encodedPassword);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// Act
var result = userService.create(createDTO);
// Assert
assertThat(result).isNotNull();
assertThat(result.id()).isEqualTo(1L);
assertThat(result.name()).isEqualTo(createDTO.name());
verify(userRepository).existsByEmail(createDTO.email());
verify(passwordEncoder).encode(createDTO.password());
}
@Test
@DisplayName("Deve lançar exceção para email duplicado")
void shouldThrowExceptionForDuplicateEmail() {
// Arrange
var createDTO = UserTestFactory.createValidUserDTO();
when(userRepository.existsByEmail(createDTO.email())).thenReturn(true);
// Act & Assert
assertThatThrownBy(() -> userService.create(createDTO))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("Email já está em uso");
}
}
2. Testes de Controller (Slice Tests)
Características:
- Testam camada web isoladamente
- Mockam services
- Testam serialização/deserialização JSON
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@DisplayName("Deve criar usuário com sucesso")
void shouldCreateUserSuccessfully() throws Exception {
// Arrange
var createDTO = UserTestFactory.createValidUserDTO();
var responseDTO = UserTestFactory.createUserResponseDTO();
when(userService.create(any(UserCreateDTO.class))).thenReturn(responseDTO);
// Act & Assert
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDTO)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(responseDTO.id()))
.andExpect(jsonPath("$.name").value(responseDTO.name()));
verify(userService).create(any(UserCreateDTO.class));
}
@Test
@WithMockUser(roles = "ADMIN")
@DisplayName("Deve listar usuários com perfil admin")
void shouldListUsersWithAdminRole() throws Exception {
var users = List.of(
UserTestFactory.createUserResponseDTO("João", "joao@test.com"),
UserTestFactory.createUserResponseDTO("Maria", "maria@test.com")
);
var page = new PageImpl<>(users);
when(userService.findAll(any(Pageable.class), isNull())).thenReturn(page);
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpected(jsonPath("$.content", hasSize(2)));
}
}
3. Testes de Repository (Data Layer)
Características:
- Testam acesso a dados
- Usam banco H2 em memória
- Testam queries customizadas
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("Deve encontrar usuário por email")
void shouldFindUserByEmail() {
// Arrange
var user = UserTestFactory.createUserEntity();
entityManager.persistAndFlush(user);
// Act
var foundUser = userRepository.findByEmail(user.getEmail());
// Assert
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getName()).isEqualTo(user.getName());
}
@Test
@DisplayName("Deve verificar se email existe")
void shouldCheckIfEmailExists() {
var user = UserTestFactory.createUserEntity();
entityManager.persistAndFlush(user);
assertThat(userRepository.existsByEmail(user.getEmail())).isTrue();
assertThat(userRepository.existsByEmail("inexistente@test.com")).isFalse();
}
}
4. Testes de Integração
Características:
- Testam fluxos completos
- Usam contexto Spring completo
- Simulam cenários reais
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class AuthFlowIT {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("Deve realizar fluxo completo de registro e login")
void shouldPerformCompleteRegistrationAndLoginFlow() throws Exception {
// 1. Registrar usuário
var registerDTO = UserTestFactory.createValidUserDTO();
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerDTO)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value(registerDTO.email()));
// 2. Fazer login
var loginDTO = new LoginRequestDTO(registerDTO.email(), registerDTO.password());
var loginResult = mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists())
.andReturn();
// 3. Usar token para acessar endpoint protegido
var loginResponse = objectMapper.readValue(
loginResult.getResponse().getContentAsString(),
TokenResponseDTO.class
);
mockMvc.perform(get("/api/users/profile")
.header("Authorization", "Bearer " + loginResponse.token()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(registerDTO.email()));
}
}
Configuração de Testes
Configuração de Propriedades
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
# JWT para testes
jwt:
secret: test-secret-key-for-testing-only
expiration: 3600000
Test Data Factories
public class UserTestFactory {
public static UserCreateDTO createValidUserDTO() {
return createValidUserDTO("João Silva", "joao@test.com");
}
public static UserCreateDTO createValidUserDTO(String name, String email) {
return new UserCreateDTO(name, email, "password123");
}
public static User createUserEntity() {
return User.builder()
.name("João Silva")
.email("joao@test.com")
.password("$2a$10$encoded.password.hash")
.colorTheme("light")
.createdAt(Instant.now())
.roles(Set.of(createUserRole()))
.build();
}
public static UserResponseDTO createUserResponseDTO(String name, String email) {
return new UserResponseDTO(1L, name, email, Instant.now(), Set.of("USER"));
}
private static Role createUserRole() {
return Role.builder()
.id(1L)
.name("USER")
.description("Usuário padrão")
.build();
}
}
Comandos e Execução
Comandos Maven Essenciais
# Executar todos os testes
./mvnw test
# Executar com cobertura
./mvnw test jacoco:report
# Executar testes específicos
./mvnw test -Dtest=UserServiceTest
./mvnw test -Dtest="**/*Test" # Apenas unitários
./mvnw test -Dtest="**/*IT" # Apenas integração
# Ver relatório de cobertura
open target/site/jacoco/index.html
Configuração JaCoCo
Plugin de Cobertura
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<configuration>
<excludes>
<exclude>**/*Application.class</exclude>
<exclude>**/config/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/entity/**</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
Boas Práticas
Naming Conventions
// Padrão AAA (Arrange, Act, Assert)
@Test
@DisplayName("Deve criar usuário quando dados válidos fornecidos")
void shouldCreateUser_WhenValidDataProvided() {
// Arrange - Preparar dados
var createDTO = UserTestFactory.createValidUserDTO();
// Act - Executar ação
var result = userService.create(createDTO);
// Assert - Verificar resultado
assertThat(result).isNotNull();
}
// Testes de exceção
@Test
@DisplayName("Deve lançar BusinessException quando email duplicado")
void shouldThrowBusinessException_WhenEmailAlreadyExists() {
// Arrange & Act & Assert
assertThatThrownBy(() -> userService.create(createDTO))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("Email já está em uso");
}
Organização de Testes
- Um assert principal por teste
- Usar @DisplayName descritivo
- Agrupar testes relacionados com @Nested
- Usar factories para dados de teste
- Limpar estado entre testes
@Nested
@DisplayName("Criação de Usuário")
class UserCreationTests {
@Test
@DisplayName("Deve criar usuário com dados válidos")
void shouldCreateWithValidData() { /* ... */ }
@Test
@DisplayName("Deve falhar com email duplicado")
void shouldFailWithDuplicateEmail() { /* ... */ }
}
Performance dos Testes
- Usar @MockitoExtension para testes unitários
- @DataJpaTest apenas para repositories
- @WebMvcTest apenas para controllers
- @SpringBootTest apenas quando necessário
- Reutilizar contexto Spring quando possível
Métricas de Qualidade
- Cobertura mínima: 70%
- Tempo de execução: < 2 minutos para suite completa
- Testes unitários: < 30 segundos
- Testes de integração: < 90 segundos
Esta estratégia garante confiabilidade, manutenibilidade e cobertura adequada para o backend do FinBoost+, seguindo as melhores práticas da comunidade Spring Boot e Java.