Pensando em testes - Parte 3 - Achando o Norte

Na parte 2 da série Pensando em Testes falei sobre diretrizes de escrita de testes. Agora apresento um exemplo concreto de diretrizes (que provavelmente não se aplicarão ao seu caso)

Pensando em testes - Parte 3 - Achando o Norte
Foto de Pixabay: https://www.pexels.com/pt-br/foto/pessoa-segurando-compasso-cinza-e-preto-220147/

Na parte 2 desta série mencionei que é importante definir diretrizes para evitar afundarmos no caos que nossos testes podem se tornar. Temo que ali o conceito tenha ficado vago, sendo assim nesta parte 3 apresento um exemplo de documento de diretrizes para a escrita de testes.

Pensando em testes - Parte 2 - Domando o caos
Algumas ideias para melhorar a manutenibilidade do seu conjunto de testes a partir de uma visão arquitetural.

O primeiro momento em que mencionei o uso de diretrizes

Este exemplo é focado no desenvolvimento de testes de validação para o backend de um cliente. Antes de mostrar o exemplo uma série de avisos devem ser dados.

Primeiro aviso: Cada caso é único e tudo muda (o tempo todo)

Foto de Emiliano Arano: https://www.pexels.com/pt-br/foto/ocean-wave-na-hora-azul-1330219/

O exemplo que vou mostrar é específico de um projeto e não acredito que existam fórmulas mágicas. Tudo varia demais: sua equipe, cliente, ambiente, negócio... Ele: o CONTEXTO! Então você pode até pegar as diretrizes que exponho aqui como um ponto de partida, mas não é garantido que serão as mesmas ao longo do tempo (não serão).

E tem de mudar mesmo, pois o time e o cliente irão descobrir coisas ao longo do tempo.

Então: como ponto de partida poderia ser usado? Talvez, se fizer sentido para o seu contexto. Acho que serve mais pra passar um norte de como prosseguir.

💡
Apesar de contextos variarem, acho fundamental que toda diretriz use como base ou a história do projeto ou experiências passadas pelo time.

Isto JUSTIFICA as diretrizes. Evita o "técnico tiraninho" que... convenhamos, é um saco.

Segundo aviso: Liderança é necessária (mas não aquela coisa cafona de coaches e LinkedIn)

Infelizmente o termo liderança foi banalizado com o tempo. Penso nas diretrizes como um ato de "liderança inicial". A proposta de um caminho para que o time siga junto. Importante a última palavra: junto.

💡
Tenho um conceito bem pessoal de "líder": é aquele que tem a iniciativa de propor um caminho.

Depois pode até mesmo ser a pessoa que continue direcionando o time para aquela direção, mas isto nem vai existir se não houver a ousadia de propor um início.

Se você impõe diretrizes as pessoas vão ter resistência em seguir. Elas sempre devem ser propostas como um passo inicial, se possível baseando-se nas experiências passadas do time. E sabe o que é mais legal? Acredito que qualquer um possa ser líder neste sentido: líder é aquele que vai apontar um caminho, e só.

💡
Algo que todo mundo deveria aprender: é saudável DEMAIS ter discordância em um time. Se a discordância é aceita é sinal de que talvez o tirano não tenha vez.
(aprendi isto na primeira vez em que li sobre a Academia de Platão na qual todo mundo era incentivado a discutir, é algo legal pra se estudar)

Com base nisto, o que vou mostrar é apenas um exemplo que acho importante ter aqui nesta série para que todos entendam o que quero dizer com "diretrizes para testes". Aqui mostro apenas um exemplo para backend, mas existem outras opções para frontend, integrações, funcionais, etc.

Este exemplo é de um projeto específico: não quer dizer que todos sigam as mesmas regras. Talvez você discorde do que lerá na sequência, mas lembre-se: estamos falando de um contexto específico que COM CERTEZA é diferente do seu. ;)

Finalmente... o exemplo

Diretrizes para a escrita de testes de backend


Versão: 1.0 - 2/4/2025
Autor: Henrique Lobo Weissmann <kico@itexto.com.br>

Introdução


Neste documento encontram-se algumas diretrizes relacionadas à escrita de testes para o projeto X. Em nossa experiência o código de testes automatizados precisa ter qualidade tão boa quanto (na minha opinião, precisa ser até mesmo superior) à do código que é validado por este.

Por esta razão seguem aqui algumas diretrizes que devem ser seguidas na implementação destes testes visando a manutenibilidade e entendimento na execução dos mesmos.

Melhor prática possível - comece pelos testes sempre


Novas funcionalidades

É comum quando pegamos uma funcionalidade para implementar não sabermos ao certo qual será o design do nosso código. Pense nos testes como sua ferramenta de design.
Se você começa pela escrita dos testes já inicia seu trabalho definindo o mais importante que são as interfaces que serão disponibilizadas pelo seu código. Isto te ajuda a planejar melhor o que deve ser feito e verificar os possíveis comportamentos esperados.

Além disto, você também terá algo muito importante: um indicador que lhe dirá quando seu código está pronto do ponto de vista funcional. A partir deste momento, é possível tanto entregar a funcionalidade para o projeto como aproveitar o momento para implementar otimizações caso necessário, e com a garantia de que você não estará quebrando o que construiu a cada otimização ou refatoração necessária.

Correção de bugs - FORTEMENTE RECOMENDADO


Se encontrou um bug no backend, a melhor prática possível consiste em, antes de tentar depurar o código, escrever um teste de regressão contra o conjunto de endpoints que irá causar o problema.

Procure o QA (Fernando) antes de tratar o bug. Obtenha junto com ele qual o conjunto de endpoints que está sendo invocado, assim como os próprios usuários e recursos necessários para reproduzir o problema localmente em seu teste.

Ao escrever este seu teste de regressão, siga os seguintes princípios:

  • Crie uma classe de teste cujo nome aponte para a issue que descreve o problema.
  • Esta classe de testes deverá estar no pacote `regressao` do seu projeto* Escreva testes de integração contra os endpoints e os valores de retorno obtidos por este* Lembre de validar as fontes de dados manipuladas para ver se o resultado esperado (o bug) ocorreu.

Com base nisto você conseguirá reproduzir os comportamentos, ganhará tempo no suporte e ainda teremos um teste que evitará a recorrência do problema reportado.
Abaixo está um exemplo real de teste de regressão que escrevi. Observe que apenas o comportamento é validado.

@AutoConfigureWebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// Observe como o nome do teste aponta já para a issue 21370 e só pra ela
public class Regressao21370 {

    @Autowired
    private InscricaoRepository participanteRepository;

    @Autowired
    private WebTestClient testClient;

    @Autowired
    private UtilAuthIntegrationTest utilAuth;

    @BeforeEach
    public void beforeEach() {
        utilAuth.init(this.testClient);
        // Talvez você queira debugar o que está ocorrendo, então
        // fica fácil definindo um timeout para o cliente HTTP
        this.testClient = this.testClient.mutate().responseTimeout(Duration.ofHours(12)).build();
    }

    /**
     * Criado com base na issue 21370
     *
     * Gestores devem ser capazes de alterar a sala do inscrito
     * Não estava mais sendo peritido que estas alterações fossem realizadas.
     * 
     * Observe como o teste busca o registro na base canônica para validar se
     * um usuário gestor (criado por um serviço auxiliar de testes) consegue ou 
     * não realizar a edição das unidades do registro
     */
    @Test
    public void testDeveSerPossivelEditar() {

        final Participante participante = participanteRepository.findByUuid("id_canonico");

        assertNotNull(participante, "Sanidade, sem participante");

        final Curso curso = participante.getCurso();
        
        final String gestorUsername = UUID.randomUUID().toString() + "@test.com";
        final String password = "SuperCurso2025*Brutal";
        final Autorizacao autorizacao = this.utilAuth.criarGestor(gestorUsername, password, evento.getCodigo());
        assertNotNull(autorizacao, "Gestor não foi criado");

        
        final String url = "/api/v1/curso/" + evento.getCodigo() + "/adhesion/participant/" + participante.getUuid() + "?active=true&actionActivateDeactivateParticipant=false";

        ParticipanteDashboardDTO request = new ParticipanteDashboardDTO();
        request.setSala(56l);
        request.setName(participante.getUsuario().getNome());
        request.setBirthdate("09/01/1979");
        request.setEnrollment(participante.getMatricula());

        ParticipanteDashboardDTO result = this.testClient.put().uri(url)
                .header("Authorization", autorizacao.getToken())
                .bodyValue(request)
                .exchange()
                .expectStatus().isEqualTo(200)
                .expectBody(ParticipanteDashboardDTO.class)
                .returnResult().getResponseBody();

        Participante after = participanteRepository.findById((Long) participante.getId()).get();
        assertEquals(after.getSala().getId(), request.getUnit(), "Não mudou a sala");
    }
}

Foco nos comportamentos - BDD

O ideal é você implementar seus testes com foco nos comportamentos, se possível, usando os princípios básicos do BDD.

Testes unitários são úteis quando desejamos validar o comportamento de detalhes de implementação do nosso código. Na nossa prática, na maior parte das vezes são úteis apenas como ferramentas de construção de código, podendo fácilmente ser descartados após a implementação ou correção estar concluída.

No caso de testes unitários faz sentido termos o uso de mocks inclusive, por que se faz necessário validar a lógica interna de comportamento do componente. O problema é que todo código que mandamos para produção interage com outros elementos da plataforma: bases de dados relacionais, outros sistemas, arquivos, configurações, etc.

A escrita de apenas testes unitários com mocks normalmente não valida estes cenários. Na realidade, há inclusive o risco de, se usados em excesso, criarem código final que não irá funcionar uma vez implantado.

Foque na escrita de testes que exercitem endpoints ou serviços macro da plataforma, de preferência de forma integrada. E todo teste deve ter um comentário tal como no exemplo a seguir, no caso de um endpoint:

/**
   POST /api/v1/usuario

   Verifica se, com todas as condições satisfeitas, é possível criar um novo usuário.
   As condições a serem satisfeitas são:

   * O corpo da requisição é válido (todos os atributos estão ok)
   ** O e-mail fornecido é único
   ** A matrícula do usuário é única
   * O usuário que faz a requisição possui permissão para a invocação do endpoint

   O resultado deve ser o código HTTP 201 contendo no corpo da requisição os dados
   básicos do usuário, somado a um UUID que permita identificar o registro no
   banco de dados
 */

Terceira melhor prática possível - seu código de testes deve ser lindo

Muita atenção ao código de testes que você escreve. Ele não é o que vai pra produção mas é o que garante que o que implantamos está funcionando.
E se o teste é difícil de manter, perdemos esta qualidade que é fundamental para a manutenção da plataforma. Se estamos usando uma linguagem orientada a objetos, atenção:

  • Vai criar um serviço de apoio? Ele é útil pra todos os testes ou só pra um conjunto? Se é só pra um conjunto, coloca o serviço no pacote a que pertence este conjunto.
  • Sua classe de testes expõe como público apenas o que de fato deve ser público (isto é, os testes)?
  • Você realmente precisa criar um serviço de apoio? Será que a escrita de funções utilitárias privadas na sua classe de teste já não resolvem seu problema?

Vou um passo além: por que não um "meta teste"? Se você tem serviços de apoio, sabe o que seria legal? Garantir que estes sejam seguros e confiáveis pra você. Escreva testes pra eles!

Siga as mesmas boas práticas de implementação que você segue no código de produção. Na minha opinião o código dos testes deve ser visto como algo tão importante (na minha opinião, MAIS) do que o código validado.

O que deve ser evitado

Herança

Por muito tempo usamos como base a classe BaseTest para a escrita das novas classes de teste neste projeto. A ideia era que nesta classe estariam presentes funcionalidades que seriam compartilhadas por todos os casos de testes a serem escritos na plataforma.

Com o tempo, no entanto, observou-se que muitas vezes eram incluídas nesta classe funcionalidades que atendiam apenas um conjunto muito pequeno de classes de testes (em alguns casos, apenas uma classe (!!!)). Por esta razão a escrita de qualquer nova classe de testes que estenda outra, a não ser que muito bem justificada a razão, deve ser evitada.

E qualquer nova classe filha de BaseTest, dado o fato desta estar marcada como @Deprecated, está descartada e não deve ser aceita no processo de revisão de código.

Precisa de herança mesmo??: a classe base deve estar no pacote do seu teste e também deverá ter comentários explicando o por quê de sua existência.

Alternativa: use composição. Verifique no seu projeto de backend se há um pacote de utilidades nos testes. Use estes serviços como dependências das suas classes de testes. Já há funcionalidades prontas para a autenticação e criação de usuários por exemplo.

Quando observar que há a oportunidade de se criar algo que será reaproveitado por outras funcionalidades do projeto, priorize a escrita deste serviço auxiliar.

Criação desnecessária de registros no banco de dados de teste

O ambiente de desenvolvimento e CI já possui uma base de dados canônica que contempla os principais casos de uso do projeto.

Sempre que possível use esta base de dados para a escrita dos seus testes. Evite criar registros desnecessários, pois muitas vezes estes apenas tornam mais lenta a execução dos testes no nosso mecanismo de integração contínua.

Evite mocks quando o objetivo da funcionalidade for manipular dados

Se você vai validar uma funcionaidlade que irá manipular bases de dados (SGBD, S3, invocar outros serviços), evite escrever seus testes contra mocks.

Estes não garantem que a funcionalidade de fato alterou as bases de dados tal como previamente implementado.

Testes privados e públicos - No repositório apenas testes PÚBLICOS

Se você escreveu um teste de desenvolvimento apenas como base para a sua implementação, não o submeta para o repositório. Se o fizer, já o marque com a anotação `@Deprecated` do Java para que possa ser removido no futuro.

Em um primeiro momento pode ser difícil para você identificar um teste como privado e um teste público. Então seguem algumas dicas que vão te ajudar:

  • Se o teste foi escrito só pra executar o código que você está escrevendo, provavelmente é privado e pode ser descartado.
  • Se o teste não tem nenhum comentário explicando o que está sendo validado, pode ser que seja um teste privado.
  • Se faça a seguinte pergunta: "que comportamento REAL do sistema estou validando com este teste?". Demorou pra responder? É um sinal.

Mas o que é um teste público afinal? Essencialmente, é um teste que paga o tempo que leva a sua execução no processo de integração contínua e que mostra como o sistema deve se comportar.

Ele costuma ter uma série de atributos que o identificam:

  • Há um comentário claro dizendo o que está sendo validado. Qual o comportamento e por que este é o comportamento correto ou incorreto esperado.
  • Qualquer pessoa ao ler tanto o comentário quanto o nome da função pública que executa o teste consegue entender por que aquele teste é ou não importante.

Atenção para o último ponto que citei acima. Se está claro que o teste não é importante, ótimo! É um teste que podemos apagar e que vai economizar tempo de execução na integração contínua!

Fim do exemplo e notas finais

Observe que interessante as diretrizes destas diretriz (meta diretrizes).

  • Pra cada ponto há uma justificativa. Não é só um conjunto de frases soltas como se fossem mandamentos. Há abertura para discussão aqui.
  • Você escreve em português, não em Java. Assim tanto a equipe técnica quanto a gerencial, quanto o cliente e qualquer auditor pode entender sobre o que você está falando.
    • Pode haver também um guia de estilo ou coisas similares, mas acho o foco no Português muito mais importante. Não são todos do seu time que vão ter a mesma maturidade técnica.
  • É apenas um norte, que vai mudar no futuro.

Neste mês retomei esta série, espero que lhes seja útil no futuro. Até os próximos posts (este mês tem mais!)

Pensando em testes
Uma das grandes dificuldades no desenvolvimento de software é a escrita de testes e validações. Muitas equipes tem dificuldade em iniciar a escrita dos testes, mas sabe o que é ainda mais difícil? Manter toda esta base de código! Esta série de posts tem como objetivo te ajudar neste segundo

Mantido por itexto Consultoria