Dica Java: Métodos estáticos não! #003

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() {

Mar 25, 2025 - 22:03
 0
Dica Java: Métodos estáticos não! #003

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.