Alura > Cursos de Programação > Cursos de Node.JS > Conteúdos de Node.JS > Primeiras aulas do curso Testes com TypeScript: refatoração, TDD e boas práticas

Testes com TypeScript: refatoração, TDD e boas práticas

Aplicando boas práticas em testes - Apresentação

Boas-vindas ao curso de Boas Práticas com Testes!

Sou Emerson Laranja, instrutor da Escola de Programação.

Audiodescrição: Emerson é um homem negro de barba e cabelos escuros. Usa óculos de grau quadrado, aparelho ortodôntico e uma camiseta verde. Ao fundo, uma parede lisa com iluminação degradê do verde ao azul.

Este conteúdo é destinado a quem deseja se aprofundar em testes, utilizando boas práticas observadas no Node.js.

O que aprenderemos

Nessa jornada, você aprenderá o como e quando usar classes dublês e espiões. Além de como utilizar a Inteligência Artificial para gerar mais casos de testes, aumentando a cobertura de testes dos seus módulos.

Construiremos módulos a partir dos testes, o desenvolvimento guiado por testes, conhecido como TDD. Dentro do TDD faremos testes unitários, de integração, vamos separar o banco de dados de produção e o banco de dados para testes, além outras boas práticas.

Exploraremos tudo isso em uma API ToDo list, ou seja, lista de tarefas, onde, dando continuidade ao projeto desta formação, criaremos as funcionalidades de listar e atualizar tarefas.

Pré-requisitos

Para que você tenha o melhor proveito, é importante que você tenha assistido os outros dois cursos dessa formação sobre SOLID e padrões de projeto. Afinal, nosso projeto é uma continuação.

Convidamos você a ir além de assistir aos vídeos: faça a leitura e as atividades deste curso, converse com outros estudantes na nossa comunidade do Discord e, claro, tire suas dúvidas no Fórum.

Vamos lá?

Aplicando boas práticas em testes - Criando classes dublês

Começaremos relembrando nosso projeto. Temos uma API no back-end que funciona como uma ToDo list e a funcionalidade de criar e deletar tarefas.

Agora, queremos finalizar esse CRUD com a opção de listar e atualizar as tarefas, porém garantindo uma maior qualidade do nosso código. Para isso, pensaremos nessas implementações a partir de testes.

Analisando o arquivo de teste

Antes de começarmos a criar os testes de fato, vamos abrir no VS Code o projeto. Acessamos src > adapters > controllers > task > addTask.spec.ts. Esse é um arquivo que deixamos pronto para começarmos a pensar em boas práticas na criação dos testes. Vamos analisá-lo.

//Código omitido

describe("AddTask Controller", () {

    test("Deve chamar AddTask com valores corretos", async ()
        const httpRequest = {
            body: {
                title: "any_title",
                description: "any_description",
                date: "30/06/2024",
    },
};
await MongoManager.getInstance().connect(env.mongoUrl);
const taskMongoRepository = new TaskMongo Repository();
const dbAddTask = new DbAddTask(taskMongo Repository);
const addTaskController = new AddTaskController(
    dbAddTask,
    addTaskValidationCompositeFactory()
);

const httpResponse = await addTaskController.handle(httpRequest);

expect(httpResponse.statusCode).toBe(201);
expect(httpResponse.body.title).toBe("any_title");
expect(httpResponse.body.description).toBe("any_description");
expect(httpResponse.body.date).toBe("30/06/2024");
});

//Código omitido

Na linha 11 temos um test() e o que ele faz. Nesse caso ele deve chamar o AddTask com valores corretos. Abaixo, na linha 12 criamos uma requisição HTTP passando alguns dados falsos como um any_title, any_description e uma data qualquer.

Próximo à linha 19, conectamos o teste ao banco de dados e fazemos a instância do nosso repositório. Fizemos as implementações anteriormente.

Após, chamamos o dbedtask, que é quem sabe adicionar uma tarefa e passamos essas informações para o controller. Assim como chamamos quem faz as validações ao adicionar uma tarefa.

Na linha 27, chamamos o método handle() e recebemos uma resposta que armazenamos em httpResponse. Em seguida, após essa parte de instanciação, temos o teste.

Nele, verificamos se o que recebemos possui status 201, ou seja, que a tarefa foi criada, e também se possui os campos que passamos, any_title, any_description e a data 30/06/2024.

Agora podemos executar esse teste. Para isso, abrimos o arquivo package.json. Além do start, deixamos mais dois scripts, o test, onde visualizaremos o teste, e o test:verbose, que é a visualização com mais detalhes.

Como só queremos executar o teste, abrimos o terminal e passamos o comando npm run test, seguido de "Enter". Após ser executado, recebemos uma mensagem de erro relacionado ao timeout.

Isso significa que na linha 19 ele tentou conectar com o banco de dados, mas não teve sucesso. Isso porque o servidor realmente não foi inicializado.

Inicializando o Docker

Precisamos inicializá-lo, pois deixamos tudo no Docker. Então, em um novo terminal, passamos o comando docker compose up, seguido de "Enter".

docker compose up

Após executar, voltamos ao terminal anterior e executamos novamente o npm run test. Após aguardar alguns instantes, recebemos a mensagem que o teste passou.

Identificando problemas no código

Vamos entender qual é o problema desse código. Analisando-o novamente, o trecho da linha 12 até a 25 é para lidar com instanciações, o que atrapalha na legibilidade e manutenção do código.

A ideia do nosso teste é receber essas informações já prontas e apenas testar o que queremos, no caso verificar se os campos recebidos são iguais aos que passamos e se o status é 201.

O outro problema é relacionado ao banco de dados. Vamos acessá-lo para conseguirmos visualizar. Criamos um novo terminal e passamos o comando docker exec -it mongodb mongosh, para acessar o container. Dentro, usaremos nossa tabela de testes, então passamos use tdd e pressionamos "Enter".

use tdd

Após, para exibir as tabelas, usamos o comando show tables.

show tables

Já temos a tabela de testes criadas, então para verificar as que possuem esse teste, executaremos o comando db.tasks.find(), assim ele buscará todas as tarefas do banco.

db.tasks.find()

Aqui está o problema. Repare que nosso teste foi adicionado no banco de produção. Assim, no dia a dia teremos os dados que são fictícios misturados com os nossos dados reais, que também é um problema.

Para resolver, removeremos o trecho de código em que chamamos as classes reais que estão sendo usadas em produção. O que estamos nos preocupando é com o valor retornado. Então, criaremos algumas classes que vão simular esses valores retornados.

Criando classes de simulação

Apagamos da linha 9, onde temos o await, até a linha 21, onde temos a const dbAddTask. O controller precisa receber a simulação de quem sabe adicionar uma tarefa e também de quem sabe fazer uma validação.

Para isso acima do describe(), próximo à linha 10, criaremos uma nova classe passando class AddTask e a nomeamos de Stub, relacionado a essa simulação.

Na mesma linha, passamos implements AddTask{}. Feito isso, clicamos em AddTask e depois em "AddTask" novamente para fazemos a implementação na pasta de "usecases".

Nas chaves, precisamos passar a descrição do método. Para isso, usaremos o atalho do VS Code de correção rápida clicando em AddTaskStub e depois em "Implementar a interface 'AddTask'".

Antes de add(), como se trata de uma adição, simularemos um método assíncrono, assim como é feito com o banco de dados. Depois, na linha abaixo, passamos return Promise.resolve({}).

Quando a Promise for resolvida, queremos ter como retorno a tarefa que possui um id:"any_id", seguido dos mesmos valores que passamos na requisição, title, description e date. Ficando da seguinte forma:

//Código omitido

const makeAddTask = (): AddTask => {
  class AddTaskStub implements AddTask {
    async add(task: AddTaskModel): Promise<Task> {
      return Promise.resolve({
        id: "any_id",
        title: "any_title",
        description: "any_description",
        date: "30/06/2024",
      });
    }
  }

//Código omitido

Após, em addTaskController, podemos substituir o dbAddTask, próximo à linha 33, por new AddTaskStub(). Faremos o mesmo para o validation.

Abaixo da classe AddTaskSub, criamos outra passando class ValidationStub implements Validation{}. Clicamos em Validation para fazer a implementação do método.

Esse método não precisa ser assíncrono, precisamos apenas pensar no valor que retornaremos. No caso de sucesso, onde não ocorreu nenhum erro, o que é retornado do método Validate é void. Sendo assim, na linha abaixo podemos passar apenas return.

//Código omitido

const makeValidation = (): Validation => {
  class ValidationStub implements Validation {
    validate(data: any): void | Error {
      return;
    }
  }

//Código omitido

Em sequência, na constante addTaskController, substituiremos a classe de produção pela simulação. Então, apagamos o addTaskValidationCompositeFactory() e passamos new ValidationStub().

//Código omitido

const addTaskController = new AddTaskController(
new AddTaskStub(),
new ValidationStub()
);

//Código omitido

Com essas alterações já podemos testar novamente e verificar se está tudo correto. Abrimos o terminal onde estávamos executando os testes e passamos o comando clean para limpá-lo. Após, passamos o comando npm test e ele passa.

Conclusão e próximos passos

Observe que agora conseguimos diminuir as informações que estavam dentro do nosso teste.

Principalmente em relação à instanciação, a colocamos para fora, nas classes que demos o nome de Stub, que significa dublê em português.

É como se estivéssemos na produção de um filme de ação, onde nos cenários mais perigosos, como um saltar de um prédio pra outro, pendurado em uma teia, deixamos de lado o nosso ator principal e utilizamos os dublês.

Estamos fazendo o mesmo nos nossos testes. Não usaremos os cenários mais delicados onde estamos testando as classes de produção e sim criaremos dublês para atuarem.

Dessa forma, conseguimos diminuir a quantidade de linhas no código, deixando-o mais legível. Outro ponto de destaque é que em nenhum momento conectamos ao banco de dados. Isso significa que resolvemos o problema de misturar os dados de teste com os dados de produção.

Mas, podemos melhorar ainda mais esse teste. Onde temos quanto expect, podemos substituir por uma única linha de código. É isso que fazemos na sequência.

Até lá!

Aplicando boas práticas em testes - Espionagem com Jest

Nesse vídeo, daremos continuidade no teste que estamos corrigindo.

Melhorando a legibilidade do código

Havíamos mencionado que conseguimos excluir as quatro últimas linhas de código expect e substituir por apenas uma linha. Isso vai melhorar nossa manutenção, pois teremos que ajustar apenas uma linha, e também a legibilidade. Isso, porque, apenas de analisar a linha de código saberemos o que o teste está fazendo de ascensão.

Para isso, não vamos mais utilizar o método toBe e sim o toHaveBeenCalledWith(). Então, fazemos essa substituição na linha 46, no primeiro expect.

Após, excluímos as três linhas abaixo, referente aos outros expect(). No expect() que mantivemos, nos parênteses, mantemos apenas httpResponse.

//Código omitido

expect(httpResponde).toHaveBeenCalledWith(201);

Assim, estamos verificando se o método de resposta foi chamado com determinado parâmetro que passaremos. No caso, o que queremos saber se a resposta possui o que enviamos, ou seja, o title, a description e o date.

Para facilitar, copiamos esse trecho de código próximo das linhas 33 até a 35 e colamos nos parênteses de toHaveBeenCalledWith({}).

//Código omitido

expect(httpResponde).toHaveBeenCalledWith({
    title: "any_title",
    description: "any_description",
    date: "30/06/2024",
});

Se a nossa resposta tiver esses dados é porque, de fato, ele conseguiu criar. Então, executaremos o teste. Abrimos o terminal e passamos o comando npm test.

Após se concluído, repare que tivemos um erro de match indicando que o que recebemos e o que ele esperava não estão concordando. Nesse caso, foi recebido um objeto, mas ele esperava um mock ou spy.

Utilizando o espião

Isso aconteceu, pois o método toHaveBeenCalledWith espera essas informações. Mock e spy são estruturas parecidas e deixaremos uma atividade detalhando a diferença entre eles.

Para resolver esse problema, utilizaremos o spy, o espião, que fica observando até uma determinada chamada. Nesse caso, vamos colocá-lo para observar quando o método add(), que está dentro do AddTaskStub, foi chamado. Além disso, também observará se os itens que definimos acima também passaram.

Faremos essa modificação na prática para entendermos melhor. Na linha acima de const addTaskController, próximo à 38, escrevemos const addTaskStub.

Adicionamos o sinal de igual, recortamos a linha de código 40 new AddTaskStub(), e colamos. Após, na linha 40 passamos o addTaskStub().

//Código omitido

const addTaskStub = new AddTaskStub();
const addTaskController = new AddTaskController(
    addTaskStub,
    new ValidationStub()
);

//Código omitido

Agora, faremos esse processo de espionagem passando o método jest.spyOn() na linha 44. Como primeiro parâmetro, passamos o objeto addTaskStub, seguido de "add".

O espião está observando quando esse método for chamado. Agora, precisamos capturar a resposta desse método. Então, no início da linha 44, passamos const addSpy =.

//Código omitido

const addSpy = jest.spyOn(addTaskStub, "add");

//Código omitido

Assim, capturamos a resposta. Quando o método add foi chamado, nosso espião está capturando a resposta dessa chamada, que está dentro de addSpy.

Então, na linha 48, em expect(), apagamos o httpResponse e passamos addSpy. Assim, quando o addSpy for chamado, verificaremos se tera o título, descrição e data.


//Código omitido

const addSpy = jest.spyOn(addTaskStub, "add");

const httpResponse await addTaskController.handle(httpRequest);

expect(addSpy).toHave BeenCalledWith({
    title: "any_title",
    description: "any_description",
    date: "30/06/2024",
    });
});

//Código omitido

Se for verdade, significa que conseguimos criar a tarefa. Abrimos o terminal e executamos o comando npm rest. Feito isso, nosso teste passa.

Conclusão e próximos passos

Conseguimos fazer o mesmo comportamento com apenas com uma seção. Para isso, utilizamos o método jest.spyOn(). Usamos essa espionagem quando queremos verificar ou alterar o comportamento de um método que está dentro de um objeto.

Mas, ainda conseguimos melhorar um pouco mais esse teste. Seguindo as boas práticas, não é responsabilidade desse teste ter que lidar com as instanciações do nosso stub. Podemos melhorar isso criando algumas factors. Faremos isso na sequência. Até lá!

Sobre o curso Testes com TypeScript: refatoração, TDD e boas práticas

O curso Testes com TypeScript: refatoração, TDD e boas práticas possui 144 minutos de vídeos, em um total de 50 atividades. Gostou? Conheça nossos outros cursos de Node.JS em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Node.JS acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas