Essa dica: é muito importante para quem faz testes unitários!

Imagine uma service com 2 métodos que possuem a mesma validação (IF) como abaixo:

@Service
@RequiredArgsConstructor
public class PersonService {

    private static final int ADULT_AGE = 18;

    public void createAdult(final PersonDomain person) {
        if (ADULT_AGE > Period.between(person.birthdate(), LocalDate.now()).getYears()) {
            throw new UnsupportedOperationException("person.is.not.adult");
        }
    }

    public void registerCNH(final PersonDomain person) {
        if (ADULT_AGE > Period.between(person.birthdate(), LocalDate.now()).getYears()) {
            throw new UnsupportedOperationException("person.is.not.adult");
        }
    }
}

Há uma replicação de código: IF + exceção.

Se o projeto tivesse testes unitários, para ter a cobertura teria que ser algo como o código abaixo:

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    @InjectMocks
    private PersonService service;

    @Nested
    class WhenCreateAdult {

        @Test
        void shouldDoesNotThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(18));

            assertDoesNotThrow(() -> service.createAdult(person));
        }

        @Test
        void shouldDoesNotThrow2() {
            final var person = new PersonDomain(LocalDate.now().minusYears(19));

            assertDoesNotThrow(() -> service.createAdult(person));
        }

        @Test
        void shouldThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(17));

            assertThatThrownBy(() -> service.createAdult(person))
                .isInstanceOf(UnsupportedOperationException.class)
                .hasMessage("person.is.not.adult");
        }
    }

    @Nested
    class WhenRegisterCNH {

        @Test
        void shouldDoesNotThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(18));

            assertDoesNotThrow(() -> service.registerCNH(person));
        }

        @Test
        void shouldDoesNotThrow2() {
            final var person = new PersonDomain(LocalDate.now().minusYears(19));

            assertDoesNotThrow(() -> service.registerCNH(person));
        }

        @Test
        void shouldThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(17));

            assertThatThrownBy(() -> service.registerCNH(person))
                .isInstanceOf(UnsupportedOperationException.class)
                .hasMessage("person.is.not.adult");
        }
    }
}

Terá então replicação de código e de teste unitário.

É comum para evitar a replicação de código, separar em outra classe e reaproveitar o mesmo código.

public class PersonValidator {

    private static final int ADULT_AGE = 18;

    public static void verifyAdult(final PersonDomain person) {
        if (ADULT_AGE > Period.between(person.birthdate(), LocalDate.now()).getYears()) {
            throw new UnsupportedOperationException("person.is.not.adult");
        }
    }
}

Na service o código ficaria.

@Service
@RequiredArgsConstructor
public class PersonService {

    public void createAdult(final PersonDomain person) {
        PersonValidator.verifyAdult(person);
    }

    public void registerCNH(final PersonDomain person) {
        PersonValidator.verifyAdult(person);
    }
}

OK, foi resolvido a replicação do IF e do throw, porém o teste unitário ainda está replicado! E por isso é recomendado sempre usar Bean's! Evitar o uso de métodos estáticos e transformar a classe em uma Bean.

@Component
public class PersonValidator {

    private static final int ADULT_AGE = 18;

    public void verifyAdult(final PersonDomain person) {
        if (ADULT_AGE > Period.between(person.birthdate(), LocalDate.now()).getYears()) {
            throw new UnsupportedOperationException("person.is.not.adult");
        }
    }
}

A PersonService com a injeção de dependência da nova Bean.

@Service
@RequiredArgsConstructor
public class PersonService {

    private final PersonValidator validator;

    public void createAdult(final PersonDomain person) {
        validator.verifyAdult(person);
    }

    public void registerCNH(final PersonDomain person) {
        validator.verifyAdult(person);
    }
}

O teste unitário fica único.

@ExtendWith(MockitoExtension.class)
class PersonValidatorTest {

    @InjectMocks
    private PersonValidator validator;

    @Nested
    class WhenVerifyAdult {

        @Test
        void shouldDoesNotThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(18));

            assertDoesNotThrow(() -> validator.verifyAdult(person));
        }

        @Test
        void shouldDoesNotThrow2() {
            final var person = new PersonDomain(LocalDate.now().minusYears(19));

            assertDoesNotThrow(() -> validator.verifyAdult(person));
        }

        @Test
        void shouldThrow() {
            final var person = new PersonDomain(LocalDate.now().minusYears(17));

            assertThatThrownBy(() -> validator.verifyAdult(person))
                .isInstanceOf(UnsupportedOperationException.class)
                .hasMessage("person.is.not.adult");
        }
    }
}

E o teste unitário na PersonService torna-se apenas um verify.

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    @InjectMocks
    private PersonService service;

    @Mock
    private PersonValidator validator;

    @Nested
    class WhenCreateAdult {

        @Test
        void shouldDoesNotThrow() {
            final var person = new PersonDomain(LocalDate.now());

            assertDoesNotThrow(() -> service.createAdult(person));

            verify(validator).verifyAdult(person);
        }
    }

    @Nested
    class WhenRegisterCNH {

        @Test
        void shouldDoesNotThrow() {
            final var person = new PersonDomain(LocalDate.now());

            assertDoesNotThrow(() -> service.registerCNH(person));

            verify(validator).verifyAdult(person);
        }
    }
}

Obtém-se o mesmo resultado com o mesmo objetivo e ainda mantém boas práticas de código e testes.

No exemplo foi utilizado apenas um método com retorno void e utilizado apenas em dois locais do sistema, porém esse exemplo se aplica a cenários mais complexos onde condicionais ou estados de objetos influenciam e alteram os comportamentos dos componentes do sistema em N locais. Essa dica torna mais fácil, mais eficiente e menos desgastantes os testes unitários.