Em testes unitários em Java, com JUnit, existem apenas 2 cenários que necessitam testar exceções:
- try-catch's explícitos
- disparo de exceções explícitas
Por exemplo nos dois métodos com os cenários abaixo:
@Service
@RequiredArgsConstructor
public class ExampleService {
private final Repo repo;
private final Integrator integrator;
public Person getById(final UUID id) {
return repo.findById(id)
.orElseThrow(() -> new IllegalArgumentException("person.not.found")); //possivelmente customizada
}
public Person create(final Person person) {
try {
return integrator.create(person);
} catch (final Exception exception) {
//lógica do tratamento da exceção
throw exception;
}
}
}
Os testes de ambos os cenários seria:
@ExtendWith(MockitoExtension.class)
class ExampleServiceTest {
@InjectMocks
private ExampleService service;
@Mock
private Repo repo;
@Mock
private Integrator integrator;
@Nested
class WhenGetById {
@Test
void shouldThrow() {
final var id = UUID.randomUUID();
when(repo.findById(id))
.thenReturn(Optional.empty());
final var exception = assertThrows(
IllegalArgumentException.class, () -> service.getById(id)
);
assertEquals("person.not.found", exception.getMessage());
}
}
@Nested
class WhenCreate {
@Test
void shouldThrow() {
final var person = new Person("João");
when(integrator.create(person))
.thenThrow(IllegalArgumentException.class);
assertThatThrownBy(() -> service.create(person))
.isInstanceOf(IllegalArgumentException.class);
}
}
}
E o teste da exceção termina aqui, as classes que irão fazer a injeção da dependência da service não devem simular/mockar exceções no uso desses métodos (a não ser que haja try/catch explícito).
Simular/mockar exceções em outros cenários é ineficaz e infértil. Por exemplo o método:
public Person update(final Person person) {
return integrator.update(person);
}
E fazer esse teste:
@Nested
class WhenUpdate {
@Test
void shouldThrow() {
final var person = new Person("João");
when(integrator.update(person))
.thenThrow(IllegalArgumentException.class);
assertThatThrownBy(() -> service.update(person))
.isInstanceOf(IllegalArgumentException.class);
}
}
Sucesso no teste, porém é inútil. Pode-se fazer um paralelo com ter que testar NullPointerException em cada get ou uso de objeto.
Dica extra: quando usar o assertThatThrownBy ou assertThrows ?
assertThatThrownBy quando precisa testar apenas coisas básicas da exceção (mensagem, tipo, etc...) e assertThrows quando precisa testar estados e/ou comportamentos de alguma exceção customizada.
Em aplicações Spring Boot que possuem camada WEB (normalmente Rest) é normal termos uma camada Handler e/ou @ControllerAdvice onde tem o tratamento global de exceções. O teste que é realmente efetivo para essa camada é simular uma controller e mockar o disparo de uma exceção em alguma camada (service ou mapper) para que o tratamento global seja acionado e resultar no JSON customizado de erros esperado (papo para outro post).