4 dicas para aumentar a legibilidade dos seus testes

henriqueln7

Henrique Lopes Nóbrega

Posted on February 22, 2024

4 dicas para aumentar a legibilidade dos seus testes

Como pessoas desenvolvedoras, estamos sempre buscando escrever códigos manuteníveis, cuja evolução seja fácil. Com esse intuito, escrevemos testes para acompanhar nosso código e cobrir cenários esperados na execução do mesmo, mas às vezes não temos o mesmo rigor de qualidade para escrever o código de teste como temos para escrever o código de produção.

Assim como devemos nos preocupamos em aplicar diferentes técnicas para termos um bom código, como o uso de variáveis descritivas, a eliminação de duplicações e a criação de boas abstrações, devemos ter a mesma preocupação sobre nosso teste! Afinal, ele também é código e terá evolução como o resto do sistema.

Portanto, vamos ver algumas dicas para aumentar a legibilidade do seu código de teste que você pode aplicar agora mesmo? 👇

Obs.: Os exemplos usam Java e JUnit mas não há nada muito específico da linguagem e biblioteca. Cada snippet de código pode ser facilmente alterado para outro contexto, como Python com Pytest, Javascript com Jest, etc.

1. Estrutura do teste - Arrange, Act, Assert (AAA)

Essa estratégia é uma modificação no estilo de código com o intuito de facilitar a leitura. Muito parecida com a Given When Then, onde seu teste consiste de 3 etapas:

  • Preparação dos dados (Arrange)
  • Execução da função/método que está sendo testada (Act)
  • Verificação se o retorno da função está de acordo com o esperado (Assert)

Para demonstrar essa estratégia, vamos ver o mesmo teste, sem respeitar e respeitando o AAA:

//Sem respeitar o AAA. Perceba que é difícil,
//apenas batendo o olho, entender o que cada
//parte do teste está fazendo
@Test
void bookingSucceedsWhenRoomAvailable() {
    Room singleRoom = new Room(1L, "Single");
    Hotel hotel = new Hotel();
    hotel.addRoom(singleRoom, 20);
    Guest guest = new Guest();
    assertTrue(guest.bookRoom(hotel, singleRoom, 2));
    assertEquals(18, hotel.getRoomAvailability(singleRoom));
}
Enter fullscreen mode Exit fullscreen mode
// Perceba que, apenas olhando rapidamente,
//conseguimos ver 3 blocos distintos de código.
//Conseguimos ir diretamente para a parte que
//nos interessa sem precisar ver todo o bloco de código.

@Test
void bookingSucceedsWhenRoomAvailable() {
    //Arrange
    Room singleRoom = new Room(1L, "Single");
    Hotel hotel = new Hotel();
    hotel.addRoom(singleRoom, 20);
    Guest guest = new Guest();

    //Act
    boolean bookingSucceded = guest.bookRoom(hotel, singleRoom, 2);

    //Assert
    assertTrue(bookingSucceded);
    assertEquals(18, hotel.getRoomAvailability(singleRoom));
}
Enter fullscreen mode Exit fullscreen mode

Essa é uma estratégia bem simples, mas que ajuda bastante no dia a dia. Manter a estrutura consistente nos seus testes simplifica o esforço de quem vai lê-los.

2. Use nomes descritivos

Essa dica parece óbvia, mas ela é sempre útil. Algumas heurísticas para um bom nome de teste:

  • ❌ Evite nome de testes que apenas replicam o nome do método testado, como testDeposit (para o método deposit). Não é fácil inferir o comportamento do teste com essa nomenclatura.
  • ✅ Coloque o cenário e o resultado esperado no nome do teste.
    • O comportamento do teste fica explícito.
    • Força a ter diferentes testes para diferentes cenários.
    • Quando um teste falha, você imediatamente consegue entender qual foi a causa da falha.
  • No caso do Java, O JUnit 5 provê a anotação @DisplayName, que facilita bastante a visualização do nome do teste. E por receber uma string, você pode passar um nome com espaços e acentos, o que não é possível apenas nomeando o método diretamente. Recomendo MUITO o uso.
//Hmm.. Pelo nome, o que vou testar do método isValid? 
//Esse meu token é válido? Se não for válido, por que ele não é válido?
@Test
void testIsValid() {
    Token token = new Token("abc");
    boolean isValid = token.isValid();
    assertFalse(isValid);
}
Enter fullscreen mode Exit fullscreen mode

// Ahh... Esse teste foca no cenário que o token não tem o tamanho adequado e, portanto, é um token inválido! :)
@Test
@DisplayName("return false when token has no necessary length")
void returnFalseWhenTokenHasNoNecessaryLength() {
    Token token = new Token("abc");
    boolean isValid = token.isValid();
    assertFalse(isValid);
}
Enter fullscreen mode Exit fullscreen mode

3. Use funções auxiliares, muitas

Funções auxiliares (também conhecidas como helpers) são pequenas funções que extraem um comportamento repetitivo do código. No caso dos testes, geralmente elas criam dados para serem usados no teste (a próxima dica é sobre isso) ou facilitam com a infraestrutura necessária para rodar o teste.

Usando funções auxiliares você reduzirá bastante a carga cognitiva do seu método de teste, facilitando que a pessoa que está lendo foque apenas no que é específico do teste, não necessitando ocupar memória de trabalho com detalhes de implementação que não farão diferença para o entendimento do teste.

Essa é uma técnica que eu uso diariamente e que, sem dúvidas, aumenta a legibilidade dos testes que crio.

Vamos ver um exemplo para facilitar? Aqui simulei que tenho uma API que retorna álbuns filtrados por determinada cantora, fazendo uma busca por palavra chave (ex.: "Taylor" dá match com "Taylor Swift")

@Test
@DisplayName("return filtered albums by author")
void returnFilteredAlbumsByAuthor() throws Exception {

    List<Album> albums = List.of(
            new Album(1L, "Taylor Swift", "Red"),
            new Album(2L, "Lana Del Rey", "Blue Banisters"),
            new Album(3L, "Taylor Swift", "1989")); 

    albumRepository.saveAll(albums);

    String responseJson = mockMvc.perform(get("/albums?author=Taylor"))
                            .andExpect(status().is(200))
                            .andReturn()
                            .getResponse().getContentAsString();
    List<AlbumDTO> filteredAlbumsResponse = mapper.readValue(responseJson, 
new TypeReference<List<AlbumDTO>>(){});

    assertThat(filteredAlbumsResponse)
            .extracting(AlbumDTO::getId)
            .containsOnly(1L, 3L);
}

Enter fullscreen mode Exit fullscreen mode

Viu como precisamos entender diferentes abstrações ao mesmo tempo que muitas vezes não tá nem ligada com o propósito do teste? Precisamos entender como transformar a String em uma List<AlbumDTO>, como usar corretamente a API do mockMvc corretamente, entre outros.

Podemos simplificar usando funções auxiliares, como no teste abaixo:


@Test
@DisplayName("return filtered albums by author")
void returnFilteredAlbumsByAuthor() throws Exception {
    insertIntoDatabase(
        new Album(1L, "Taylor Swift", "Red"),
        new Album(2L, "Lana Del Rey", "Blue Banisters"),
        new Album(3L, "Taylor Swift", "1989")); 
      );

    String responseJson = requestAlbumsByAuthor("Taylor");

    assertThat(toDTOs(responseJson))
            .extracting(AlbumDTO::getId)
            .containsOnly(1L, 3L);
}
Enter fullscreen mode Exit fullscreen mode

Agora o código possui uma linguagem mais natural, tornando a leitura mais fácil. Quem for ler o teste para entender a regra de negócio não precisa saber exatamente qual API estamos usando para fazer as requisições HTTP ou como estamos convertendo o JSON para uma coleção de objetos de domínio.

E uma vantagem interessante à medida que começamos a usar mais e mais funções auxiliares em nosso teste é que, ao termos uma mudança de contrato em algum método testado, geralmente a quantidade de locais para alterar se tornar menor por centralizarmos boa parte das operações nas funções auxiliares.

4. Apenas mostre informação necessária para seu teste

Bons testes devem incluir apenas informações relevantes ao teste, escondendo ruído. É normal, principalmente para classes maiores, termos testes que fazem a instanciação de vários e vários objetos para descobrirmos que o teste é influenciado apenas por um parâmetro passado em determinado momento.

Para nosso exemplo, vamos imaginar o método canPayWithPix, que retorna true apenas se o User tem o endereço preenchido e o email confirmado.

@Test
@DisplayName("return true when user has filled address and confirmed email")
void returnTrueWhenUserHasFilledAddressAndConfirmedEmail() {
    var user = new User("Henrique", "Lopes Nóbrega", new Email("henrique@email.com.br").asConfirmed(), LocalDate.parse("2000-09-12"), "+5584.....", "Rua Pipipi Popopo", "12345678910");

    boolean canPayWithPix = user.canPayWithPix();

    assertTrue(canPayWithPix);
}
Enter fullscreen mode Exit fullscreen mode

Observe quantas informações a mais que é desnecessária para o teste passamos (e esse é um exemplo modesto, entidades como User tendem a ser maiores em sistemas reais). Passamos o telefone, número de celular, etc. Nenhuma dessas informações têm o poder de alterar o resultado do método que estamos testando, então podemos apenas removê-las para simplificar a leitura do nosso teste.

Para fazer isso, vamos usar funções auxiliares. Criamos uma função auxiliar anUser que esconde todos os dados que são desnecessários e usamos no lugar de construir o objeto via construtor diretamente.

@Test
@DisplayName("return true when user has filled address and confirmed email")
void returnTrueWhenUserHasFilledAddressAndConfirmedEmail() {
    var user = anUser("Rua Pipipi Popopo", confirmedEmail("henrique@email.com.br");

    boolean canPayWithPix = user.canPayWithPix();

    assertTrue(canPayWithPix);
}

private void anUser(String address, Email email) {
    return new User("Henrique", "Lopes Nóbrega", email, LocalDate.parse("2000-09-12"), "+5584.....", address, "12345678910");
}

private void confirmedEmail(String email) {
    return new Email(email).asConfirmed();
}
Enter fullscreen mode Exit fullscreen mode

Perceba que apenas mostrando o necessário, é mais fácil ler o código e entender o porquê o usuário pode pagar com PIX. E como teremos diferentes testes para os outros cenários desse método, fica fácil reaproveitar as funções auxiliares entre os testes.

⚠️ Mas cuidado para não esconder informação demais! Idealmente, toda informação necessária para que o teste seja executado de forma bem sucedida deve estar visível dentro do método de teste, não dentro do método auxiliar. Esconder essa informação vai dificultar a leitura do seu teste.

@Test
@DisplayName("return true when user has filled address and confirmed email")
void returnTrueWhenUserHasFilledAddressAndConfirmedEmail() {
    var user = anUser(); ///ué... como sei que o usuário é válido pra pagar com PIX, se nem sei qual o endereço dele aqui??

    boolean canPayWithPix = user.canPayWithPix();

    assertTrue(canPayWithPix);
}

private void anUser() {
    return new User("Henrique", "Lopes Nóbrega", new Email("henrique@email.com.br").asConfirmed(), LocalDate.parse("2000-09-12"), "+5584.....", "Rua Pipipi Popopo", "12345678910"); //Tenho que navegar até minha função auxiliar para entender que o endereço já está preenchido. No método de teste eu não tenho essa informação.
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Em resumo, a legibilidade dos testes é tão crucial quanto a do código de produção, impactando diretamente a manutenção e a qualidade do software, e deveríamos manter o mesmo rigor de qualidade que possuímos para o código de produção para os testes também.

Nesse artigo foquei em algumas técnicas que acredito que podem facilitar a legibilidade do seu teste:

  • Adotar uma estrutura consistente entre os testes
  • Usar nomes descritivos
  • Aplicar funções auxiliares e mostrar apenas informações essenciais para o teste

Ao incorporar estas dicas, você não só facilita a vida de quem lida com os testes no futuro, incluindo você mesmo, mas também eleva o padrão de qualidade do seu projeto. Lembre-se, um código de teste bem escrito é um investimento no seu produto, na sua equipe e na sua tranquilidade ao modificar e expandir funcionalidades. E você, como planeja melhorar a legibilidade dos seus testes hoje?

Conteúdos adicionais

Para quem se interessar no tema e quiser se aprofundar, o Maurício Aniche tem um excelente conjunto de livros sobre testes. Li os 3 e recomendo demais.

E alguns artigos que me ajudaram muito a entender melhor algumas práticas:

Modern Best Practices for Testing in Java (Inglês)
How to Make Your Tests Readable (Inglês)

💖 💪 🙅 🚩
henriqueln7
Henrique Lopes Nóbrega

Posted on February 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related