Construindo uma API com NestJS, PostgreSQL e Docker —Parte 6: Escrevendo testes
Recentemente comecei a estudar Node pensei em desenvolver uma API com funcionalidades básicas, que praticamente todo projeto precisa, como forma de me familiarizar com a linguagem. Para me auxiliar nessa tarefa utilizei o framework NestJS. Nesta série de tutoriais vou compartilhar o que aprendi.
Série completa:
Irei atualizando essa lista com links para as devidas partes conforme eu for postando!
- Parte 1: Criando nosso primeiro endpoint
- Parte 2: Adicionando Autenticação e Autorização
- Parte 3: Finalizando nosso CRUD de usuários
- Parte 4: Adicionando logs à nossa aplicação
- Parte 5: Enviando emails de confirmação e recuperação de senha
- Parte 6: Escrevendo testes
- Parte 7: Deploy!
Requisitos
O código da etapa anterior se encontra em um repositório no meu GitHub:
TL;DR
O código desta etapa também se encontra em um repositório no meu GitHub:
Testes em JavaScript
Escrever testes para sua aplicação é, além de boa prática, uma forma de garantir entregas com mais qualidade.
Quando se trata de JavaScript em geral a biblioteca mais utilizada para testes é o jest. Ele é um framework de testes feito para funcionar seja com Node, TypeScript, ou até mesmo com frameworks front-end como Angular, Vue ou React. Quando se cria um projeto NestJS o jest já vem instalado automaticamente, então podemos começar sem mais demoras.
Neste tutorial vamos escrever os testes para nosso UsersService. Para isso, dentro da pasta users vamos criar o arquivo users.service.spec.ts.
Vou explicar o que está acontecendo aqui:
- mockUserRepository: Ele será responsável por simular todas as chamadas ao userRepository dentro do users.service.ts. Todos os métodos que teremos que emular dentro de nosso arquivo de testes foram especificados com o valor jest.fn(). Essa funcionalidade do jest permite emular o funcionamento de métodos externos à classe que estamos testando. Isso é importante para podermos separar os escopos dos nossos testes.
- describe: Utilizamos o describe para separar as partes de nosso teste, sendo que podemos iniciar um describe dentro de outro de forma aninhada. No caso criaremos um describe para cada método do nosso UsersService.
- beforeEach: é uma função que será executada antes de cada teste a ser realizado. No caso utilizamos ela para inicializar nosso módulo e suas dependências que serão utilizadas durante os testes.
- TestingModule, Test.createTestingModule: Esses recursos são naturais do próprio NestJS para serem utilizados em conjunto com o jest. Eles simulam a inicialização de um módulo e suas dependências. É importante aproveitarmos essa parte para inicializarmos o UserRepository que o UsersService precisa para funcionar com nosso mockUserRepository que irá simular as funcionalidades do UserRepository normal.
- it: O it é utilizado para descrevermos cada teste que será realizado. No caso o primeiro teste que realizamos é se o UsersService e o UserRepository foram inicializados com sucesso.
- expect: é através do expect que verificamos se as coisas aconteceram como esperado. Nesse primeiro teste nós esperávamos que o service e o userRepository fossem inicializados com sucesso durante a criação do módulo de teste, por isso esperávamos que seus valores fossem definidos, ou seja, diferentes de undefined.
Basicamente serão essas funcionalidades que utilizaremos no decorrer dos testes para verificar se nosso código funciona como esperado. Vamos começar testando nosso método createUser:
Reparem que construímos os testes do método createUser dentro do describe do UsersService, que é onde o método se encontra.
Vamos analisar o que está acontecendo:
- Utilizamos o beforeEach para inicializarmos o valor do mockCreateUserDto, uma vez que esse é o argumento esperado pelo método createAdminUser e dentro dos nossos testes queremos ter controle sobre a entrada do método.
- O primeiro caso de testes que analisaremos será se o usuário será criado caso não tenha nenhum problema. O jest.fn() nos dá acesso ao método mockResolvedValue que simula uma execução bem sucedida de uma função assíncrona (no userRepository a função createUser é assíncrona). Vamos simular que, caso a execução seja bem sucedida, o método irá nos retornar simplesmente a string ‘mockUser’.
- Realizamos a chamada ao método que queremos testar com o valor de testes mockCreateUserDto.
- Após a execução do método createAdminUser esperamos que algumas coisas tenham acontecido. A primeira é que o método createUser do UserRepository tenha sido chamado especificamente com os argumentos mockCreateUserDto e UserRole.ADMIN. Após isso esperamos que o método tenha sido executado com sucesso e que tenha nos retornado a string ‘mockUser’ conforme especificamos no início do teste que uma execução bem sucedida do createUser irá retornar esse valor. Lembrando que isso só é possível pois inicializamos esse método com o valor jest.fn() na criação do mockUserRepository.
Vamos ao segundo caso de testes: O método createAdminUser deve retornar um erro caso as senhas sejam diferentes:
- Primeiro nós atribuímos um valor ao campo passwordConfirmation que seja diferente do valor do campo password.
- Em seguida nós simulamos a execução do método createAdminUser com um mockCreateUserDto onde as senhas não coincidem. O resultado aguardado é que a Promise seja rejeitada com o erro UnprocessableEntityException.
Podemos executar nossos testes com o comando:
$ npm run test:watch
Se tudo ocorreu bem esse será o resultado dos testes. Agora podemos testar o método findUserById:
Vamos analisar o que está acontecendo:
- Criamos um novo describe para os testes do findUserById.
- O primeiro teste realizado é caso um ID válido seja passado ao método.
- Especificamos a string ‘mockUser’ como o valor de retorno do método findOne do UserRepository caso o método seja executado com sucesso.
- Nós esperamos que, até esse momento, o método findOne ainda não tenha sido executado nenhuma vez.
- Simulamos a execução do método findUserById com um ID qualquer.
- Especificamos os campos que serão passados como parâmetro para o select do método findOne do UserRepository.
- Após a execução do método findUserById do nosso UsersService nós esperamos que o método findOne do UserRepository tenha sido executado com os argumentos ‘mockId’ e { select } sendo select o valor que definimos anteriormente.
- Nós também esperamos que o valor retornado após a execução bem sucedida de findOnde seja a string ‘mockUser’.
Vamos analisar agora o segundo teste que realizamos, que é o caso do método findOne do UserRepository não ter encontrado um usuário a partir do ID informado:
- Primeiro especificamos que o método findOne do UserRepository será executado com sucesso e retornará null.
- Depois simulamos a execução do método findUserById, onde esperamos que a função retorne o erro NotFoundException.
O próximo método da lista é o updateUsers, entretamos teremos que realizar algumas modificações nele para que ele se torne testável. Vamos pular ele por enquanto e seguir para o deleteUser:
Analisando os testes vemos que nada de novo foi feito aqui:
- Criamos um describe para os testes do método deleteUser.
- Testamos uma execução bem sucedida do método, onde o método delete do UserRepository nos retorna affected > 0.
- Testamos uma execução falha do método, onde o método delete do UserRepository nos retorna affected = 0 e então o método deleteUser retorna um erro do tipo NotFoundException indicando que não foi encontrado um usuário com o ID informado para ser deletado.
O próximo teste será para o método findUsers, que será o teste mais simples de todos:
O único teste que temos que realizar aqui é se o método findUsers do UserRepository foi chamado com o valor passado como parâmetro para o método findUsers do UsersService, nada de mais.
Por último vamos aos testes do updateUser. Primeiro vamos realizar as alterações ao método que comentei anteriormente. Para que o método se torne testável, vamos reescrever ele da seguinte forma:
Nosso users.service.ts vai ficar assim:
Reparem que:
- Antes nós buscávamos o usuário no banco de dados com o ID informado verificando sua existência, verificávamos os campos informados no updateUserDto para atualizar os respectivos campos no usuário encontrado e faziamos uma segunda chamada ao banco de dados para atualizar os dados.
- Agor nós realizamos uma chamada ao banco de dados para atualizar os dados, delegando a lógica para o método update() já padrão de um repositório TypeORM e verificamos se houve algum registro afetado no banco de dados. Se não houve um registro afetado nós retornamos um erro de que o ID é inválido. Se houve algum registro afetado significa que o ID é válido e os dados foram alterados com sucesso. Em seguida realizamos uma segunda chamada ao banco de dados para encontrar o usuário alterado, com os dados já atualizados, para podermos retornar o mesmo para o front, da mesma forma que faziamos anteriormente.
Observem que continuamos fazendo apenas duas chamadas ao banco de dados, igual anteriormente, então nossa performânce de modo geral não foi significativamente afetada.
Vamos agora escrever os testes para esse método:
Podemos observar que os testes para o método updateUser ficaram semelhantes aos do método deleteUser, com algumas pequenas excessões:
- Dentro de updateUser, caso algum dado no banco tenha sido afetado (affected > 0), realizamos uma chamada à função findUserById, então é necessário especificar um valor de retorno para a função findOne do UserRepository, uma vez que a mesma é chamada dentro de findUserById. Então nós verificamos se userRepository.update foi chamada com os argumentos corretos e posteriormente se o método updateUser do USersService retornou o usuário corretamente.
- O segundo teste, caso não tenha sido encontrado um usuário com o ID especificado, é semelhante ao do deleteUser, onde o método retorna um erro do tipo NotFoundException caso affected = 0.
Executando todos os testes escritos aqui:
E com isso finalizamos nosso tutorial sobre testes. Lembrando que o ideal seria escrever testes para todo código que escrevemos até agora, entretanto eles não são tão diferentes do que vimos neste tutorial, o que tornaria esse texto demasiadamente longo e repetitivo se fossemos escrever todos os testes aqui. Em breve publico a última parte deste tutorial, mostrando como dar deploy da nossa API!