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.
Top comments (0)