Como testar controllers em NestJS com Jest

Node.js

Como testar controllers em NestJS com Jest

Luiz Duarte
Escrito por Luiz Duarte em 26/12/2023
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Ok, que testar suas aplicações antes de enviá-las para produção é importante todo mundo já sabe. Mas será que você sabe como testar eficientemente sua aplicação escrita em NestJS?

Neste tutorial veremos como utilizar o módulo Jest para criar testes de unidade (unit tests) dos controllers em backends NestJS. É importante antes de avançar que você já tenha experiência, ao menos básico, implementando webapis com NestJS, o que pode ser aprendido nesta outra série aqui do blog. Também é desejável, embora não obrigatório, que você já conheça o básico de Jest para Node.js, o que pode ser aprendido neste outro tutorial aqui.

Veremos neste post:

Vamos lá!

Curso Node.js e MongoDB

Criando Unit Tests de Controller

Para este tutorial vamos usar o Jest como biblioteca de testes, que é muito popular atualmente e também porque ele já vem integrado “nativamente” no framework NestJS, permitindo fazer asserções de uma maneira muito simples, prática e padronizada, dando agilidade ao processo de unit testing ou TDD, caso leve a metodologia realmente a sério. Além disso ele cria informações de cobertura de testes muito legais.

Para não começar nossos estudos completamente do zero usarei o projeto visto neste outro tutorial, uma webapi de usuários (CRUD) com banco SQL cujos fontes você consegue baixar ou clonar do meu GitHub público neste link.

Recomendo que você clone esta API na sua máquina e dê uma analisada nos fontes, que são bem simples. Verá que inclusive já temos uma pasta test com um arquivo de teste e outro de configuração dentro. Isso é padrão do NestJS, não fui eu quem criou, então pode excluir o conteúdo dessa pasta. Então crie um novo arquivo de testes chamado user.controller.spec.ts. O sufixo ‘spec’ é o padrão para arquivos de teste em NestJS, então vamos manter.

Abaixo o conteúdo inicial desse arquivo, onde começaremos com o script de preparação dos testes e um teste básico se o serviço foi corretamente carregado.

Na função beforeAll, que será executada antes de todos testes da nossa suíte (literalmente “before all”), nós vamos inicializar nosso módulo de teste (TestingModule), configurando ele com o nosso UserController como controller e nosso UserService como provider, assim como faríamos em um módulo real de um backend NestJS. Repare como uso a função estática Test.createTestingModule para isso, que vai simular a inicialização real de um módulo pra gente. Na sequência, carregamos este serviço em uma variável local com a função get do objeto moduleFixture que criamos, a fim de usá-lo nos demais testes sem precisar repetir esse setup.

Como primeiro teste, vamos testar se o user controller foi carregado com sucesso, verificando apenas se está “defined” (ou seja, diferente de undefined, padrão do let). Fazemos isso com a função it, que é apenas um atalho para a função test do Jest, para soar melhor ao ler os testes (na leitura fica “it should be…”). Dentro do callback do it nós escrevemos nosso teste e ao final do mesmo usamos a função expect para analisar um resultado/variável com o auxílio de uma função de aferição, neste caso a toBeDefined, que atende ao que precisamos.

Se você já usou Jest antes, tenho certeza que nada disso é exatamente novidade, apenas as questões específicas do Nest mesmo.

Para rodar nossos testes, primeiro precisamos ir no package.json e procurar a seção jest dele, onde estão as configurações globais do Jest para esta aplicação. Ajuste para que fique como abaixo, onde incluí também uma série de configurações relacionadas a cálculos de cobertura de código (code coverage).

Agora para rodar, basta executar o comando abaixo no terminal:

E com isso você terá o seguinte resultado, se fez tudo certo.

A seguir, recomendo que você escreva um ou mais testes por conta própria, para as funções do UserController, o que deve fazer aparecer alguns desafios e problemas que trato no próximo tópico.

Testes Mockados com Jest

Um unit test é um teste automatizado que testa uma única unidade da sua aplicação, geralmente uma única função, em um único contexto. Cheio de “únicos” nesse parágrafo, não? Assim, para que nossos testes de controllers sejam realmente unitários, devemos isolá-lo de quaisquer dependências externas.

Nosso UserController não é um serviço exatamente complexo, já que ele é apenas um CRUD de usuário. No entanto ele tem um ponto fundamentalmente chato no que tange testes e que é muito comum em controllers já que eles dependem de services para funcionar. Os services por sua vez costumam depender de recursos externos, de infraestrutura, por exemplo: se você quer testar uma consulta, tem de ter massa de dados, certo? Se você quer inserir dados, tem de limpar eles ao final do teste ou até mesmo garantir que eles rodem em sequência pois um insere, outro lê e por aí vai. São vários problemas se quiser testar controllers com services que usam banco e essas abordagens que citei são todas dor de cabeça na certa, já vi elas na prática. No entanto, se usar essa abordagem de preparar recursos de infraestrutura para os testes acabará testando a infraestrutura em si e atrasando muito os testes do SEU código de fato.

Como assim Luiz, eu não deveria testar minha infraestrutura nos testes?

Dependendo do teste, até pode, mas pensa comigo: o banco de dados é uma parte da sua aplicação cujas funcionalidades não são de sua gestão ou controle, mas sim do fabricante do banco. Por exemplo, se tiver um bug no salvamento em disco de um registro, você tem como corrigir? Até poderia, já que muitos bancos são open-source, mas esse é um exemplo de aspecto da sua aplicação que você não gerencia, mas depende para o seu código funcionar, toda vez que quer escrever ou ler dados.

Esses pacotes e recursos não-gerenciáveis por você, cujo código você usa mas não mexe, são fortes candidatos a serem mockados, porque eles costumam muitas vezes tornar os seus testes dependentes, acoplados, lentos e falhos, além de exigir muito setup e cleanup, antes e depois dos testes respectivamente.

Mockados? Como assim?

Mocking é uma técnica de testes onde você cria simulações (mocks) de objetos/funções/whatever a serem utilizadas em determinados testes. Assim, ao invés de usar de fato o service real, que por sua vez usa um banco real para os testes, você pode mockar as funções do service para que o controller ACHE que o service está trabalhando, mas na verdade não. Assim, você consegue testar somente o SEU código de controller, buscando bugs nele, ao invés de ficar testando os services e o banco por tabela. Claro que para uma cobertura completa (falaremos dela mais tarde), você deverá ter unit tests para os services também, algo que já abordei neste tutorial aqui.

Vamos criar então um mock do nosso UserService usando recursos do Jest, como abaixo:

Aqui eu criei dois mocks: um mock de entidade (userMock) e um mock de service (userServiceMock), exportando os dois para que possam ser usados em nossos testes. O userMock é autoexplicativo, mas o userServiceMock requer algumas explicações.

O seu mock de userService deve estar no formato esperado pela inicialização do módulo de teste do Jest. Isso implica em ter um objeto com uma propriedade provide, informando o tipo de service, e uma propriedade useValue, que cria o service mockado. Esse service mockado deve ter todas as funções do service original que serão usadas nos testes, e em cada uma delas deve usar o jest.fn() para criar um retorno mockado para elas, simulando o efeito real do service.

Agora, volte ao user.controller.spec.ts e importe estes dois mocks:

E ajuste a inicialização do seu módulo de teste para usar o mock ao invés do service real:

Agora se você rodar o teste que já possui, não verá qualquer diferença. Mas vamos pegar como exemplo um teste de obter usuário, que normalmente exigiria um service conectado a um banco para funcionar:

Repare que aqui eu começo pegando o id falso do mock. Agora sabendo qual o retorno esperado da função (já que o service está mockado), podemos chamar a getUser (que internamente chama o service mockado) e fazemos o expect normalmente. O resultado é um belíssimo teste passando.

Mas Luiz, isso não é roubar?

Não, pois todo o código da userController.getUser está sendo testado, apenas o código interno do userService que não está. Inclusive isso permite que você baixe os fontes do repositório deste tutorial e rode os testes na sua máquina sem sequer ter um banco de dados criado!!!!

Vamos a mais um exemplo, desta vez de consulta de todos usuários:

Note que fiz a mesma coisa que antes, apenas é uma função que espera um array no final das contas, então resolvi fazer dois expects ao invés de um, o que é uma possibilidade também caso queira dar mais precisão à sua asserção.

Seguindo em frente, que tal um teste de adição de usuário?

Aqui eu fiz um setup inicial copiando o userMock (com Spread Operator) e removendo o id, já que para inserção não podemos ter id. O resultado do teste é um usuário com ID, já que ele seria gerado automaticamente pelo banco de dados!

Agora um teste de update:

Bem parecido com o anterior, certo? No update temos de passar o id do registro a ser atualizado e os novos dados. Claro, como não temos uma atualização acontecendo no banco realmente, meu teste será limitado a conferir o ID do retorno. Caso queira fazer um mock mais esperto, pode criar uma função que retorna valores mockados conforme os valores recebidos no update.

E por último, a função de exclusão:

E com isso temos todos os testes do CRUD finalizados, seu terminal deve ficar assim:

Demais, não?

Cobertura de Testes

Criar todos os testes necessários para garantir que todas as unidade do seu software realmente funcionam é o que chamamos de cobertura de código (code coverage), cuja (quase) utópica marca de 100% deve ser sempre o ideal, embora praticamente inalcançável em sistemas complexos. O quão ‘detalhado’ os seus testes serão vai muito do grau de importância que todas essas questões possuem para o seu negócio e o quanto domina  ele.

E para incrementar o resultado dos testes, vamos adicionar estatísticas de code coverage neles (cobertura de testes de código), rodando o projeto com o seguinte comando.

Com isso, não apenas os testes serão executados (todos arquivos spec.ts) como será inspecionado se seus testes cobrem as linhas, funções e ramificações do seu projeto. No meu exemplo, tive o seguinte resultado:

Note que a maioria dos arquivos do projeto está em vermelho, pois não escrevemos testes para eles, mas nosso user.controller.ts, foco deste tutorial, está com 100% de cobertura. Para melhorar a cobertura geral, podemos adicionar alguns “ignores” em arquivos deste projeto, principalmente os modules, já que eles não possuem lógica alguma, são arquivos apenas de configuração. Entendendo que não faz sentido testá-los, adicione na primeira linha de cada arquivo de module (e também no main.ts) a seguinte diretiva:

Quando você adiciona um comentário iniciado por “istanbul” você pode dizer ao Jest para ele ignorar uma linha, função ou até arquivo inteiro. No caso acima, mandei ele ignorar o arquivo inteiro.

O resultado, é um aumento na cobertura já que esses arquivos de módulo não estarão mais entrando na contabilização, veja abaixo.

Experimente escrever os testes para o app.service.ts (muito fácil) e para o main.ts, eu recomendo mandar ignorar o arquivo inteiro, usando a seguinte instrução na primeira linha do mesmo.

Juntando este conhecimento com ferramentas de Continuous Integration (CI) como CircleCI e Jenkins, podemos incluir testes de unidade automatizados em nosso build para evitar que um código suba para produção com algum bug (um dia falarei disso por aqui).

Mas e como podemos fazer testes mais amplos, como testes nos services ou End-to-End? Services eu abordei neste tutorial e E2E é assunto para outro tutorial, em breve.

Espero que tenham gostado do tutorial e que apliquem em seus projetos!


Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *