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).