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

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.