Testes Java — Spring Boot
Pirâmide de Testes
/\
/ \ E2E / API (10-15%) — @SpringBootTest + TestContainers
/----\
/ \ Integration (20-30%) — @DataJpaTest, @WebMvcTest
/--------\
/ \ Unit (55-70%) — JUnit5 puro, sem Spring context
/____________\
Por Use Case: N testes de domínio + 1-2 integração + 1 API (happy + error).
TDD — Workflow
RED → GREEN → REFACTOR
1. Teste que falha descrevendo comportamento
2. Implementação mínima
3. Refatorar sem quebrar testes
// RED
@Test
void should_add_amounts_when_same_currency() {
var price = new Money(new BigDecimal("10.00"), Currency.BRL);
var tax = new Money(new BigDecimal("1.50"), Currency.BRL);
assertThat(price.add(tax).amount()).isEqualByComparingTo("11.50");
}
@Test
void should_throw_when_different_currencies() {
var brl = new Money(new BigDecimal("10"), Currency.BRL);
var usd = new Money(new BigDecimal("10"), Currency.USD);
assertThatThrownBy(() -> brl.add(usd)).isInstanceOf(CurrencyMismatchException.class);
}
Nomenclatura
should_<resultado>_when_<cenário>
✅ should_throw_not_editable_when_order_is_confirmed
✅ should_return_201_when_create_order_with_valid_payload
❌ testOrder1
Unit Test — Domínio (sem Spring)
@DisplayName("Order.addItem")
class OrderAddItemTest {
@Test
void should_add_item_when_order_is_pending() {
var order = Order.create(CustomerId.random(), List.of());
var item = OrderItem.of(ProductId.random(), Money.of("50.00", "BRL"), 1);
order.addItem(item);
assertThat(order.items()).hasSize(1);
assertThat(order.total()).isEqualTo(Money.of("50.00", "BRL"));
}
@Test
void should_throw_when_order_is_confirmed() {
var order = Order.create(CustomerId.random(), List.of());
order.confirm();
assertThatThrownBy(() -> order.addItem(OrderItem.random()))
.isInstanceOf(OrderNotEditableException.class)
.hasFieldOrPropertyWithValue("orderId", order.id());
}
}
Mockito — Use Case
@ExtendWith(MockitoExtension.class)
class CreateOrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock ApplicationEventPublisher events;
@InjectMocks CreateOrderService service;
@Test
void should_save_order_and_publish_events() {
var cmd = new CreateOrderCommand(CustomerId.random(), List.of());
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
var result = service.execute(cmd);
verify(orderRepository).save(any(Order.class));
verify(events, atLeastOnce()).publishEvent(any());
assertThat(result.id()).isNotNull();
}
}
Preferir Fake para repositories em testes de integração de use case:
class InMemoryOrderRepository implements OrderRepository {
private final Map<OrderId, Order> store = new ConcurrentHashMap<>();
@Override public Optional<Order> findById(OrderId id) { return Optional.ofNullable(store.get(id)); }
@Override public Order save(Order order) { store.put(order.id(), order); return order; }
}
Slice Tests Spring
| Anotação | Escopo | Quando usar |
|---|---|---|
| (nenhuma) | Classe pura | Domínio, Value Objects |
@WebMvcTest |
Controller + MVC | Testar HTTP, status, JSON |
@DataJpaTest |
JPA + H2/TC | Queries, mapeamentos |
@SpringBootTest |
Contexto completo | Fluxos E2E críticos |
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mockMvc;
@MockBean CreateOrderService createOrderService;
@Test
void should_return_201_when_create_order() throws Exception {
when(createOrderService.execute(any())).thenReturn(OrderResponse.sample());
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"customerId":"%s","items":[]}""".formatted(UUID.randomUUID())))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
}
}
TestContainers
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired CreateOrderService createOrderService;
@Autowired OrderRepository orderRepository;
@Test
void should_persist_order_end_to_end() {
var result = createOrderService.execute(CreateOrderCommand.sample());
assertThat(orderRepository.findById(result.id())).isPresent();
}
}
ArchUnit — Testes Arquiteturais
@AnalyzeClasses(packages = "com.company")
class ArchitectureTest {
@ArchTest
static final ArchRule domain_should_not_depend_on_infrastructure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..");
@ArchTest
static final ArchRule controllers_should_not_access_repositories =
noClasses().that().resideInAPackage("..interfaces.rest..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure.persistence..");
}
Cobertura (JaCoCo)
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit><counter>LINE</counter><minimum>0.80</minimum></limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
Anti-Patterns
| Anti-Pattern | Problema |
|---|---|
@SpringBootTest para tudo |
Testes lentos e frágeis |
| Mock de tudo | Não valida integração real |
@Transactional em teste sem rollback |
Estado vaza entre testes |
| Testar getters/setters | Sem valor |
| Assert genérico | Mensagem de falha inútil |
Referências
~/.cursor/skills/references/tdd-examples-java.md