Como criar mocks de BD em NestJS com Jest e Prisma

Node.js

Como criar mocks de BD em NestJS com Jest e Prisma

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

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

Recentemente escrevi um artigo ensinando sobre como usar a técnica de mocking, muito popular e útil para testes unitários, com a suíte de testes Jest, para aplicações NestJS. No entanto, senti que alguns conceitos importantes não foram abordados e que podem levar a um uso equivocado da técnica, impactando principalmente na cobertura de código da sua aplicação (code coverage).

Se você usa Jest há algum tempo, deve saber que um dos grandes benefícios dele em relação a outras suítes como Tape é a coleta de coverage metrics. Ele não apenas roda suas baterias de testes automatizados como faz a análise e apresenta métricas da proporção de testes varrendo todo o seu código, como na imagem abaixo. No NestJS, o comando para rodar os testes com a cobertura habilitada é npm run test:cov

No entanto, quando começamos a mockar nossas funções, a tendência é que os testes comecem a percorrer as funções fake e não as reais, portanto caindo drasticamente a nossa cobertura de código pois ele de fato não está sendo testado, mas sim mockado. Por exemplo, o relatório abaixo é de um projeto real, que estava com +80% de cobertura de código com testes mas que caiu drasticamente por causa de apenas dois pequenos módulos que foram mockados.

Mas o que fazer nestes casos então, devemos usar mocks e abandonar coverage? Ou devemos focar em coverage e abandonar mocks?

Nenhum dos dois, devemos primeiro entender o que devemos e mockar e o que não devemos mockar!

Para conseguir realizar este tutorial você deve ter conhecimentos básicos de NestJS, obtidos nesta série por exemplo. Crie uma aplicação NestJS com Prisma do zero e a seguir use os códigos que vou mostrar, ou então pegue uma webapi de exemplo neste repositório.

Vamos lá!

O que devemos mockar?

O intuito e benefícios de utilizar mocking foi dito no último artigo, mas talvez não tenha ficado claro, então vou explicar de forma diferente.

Partes da sua aplicação como um todo não são de sua gestão ou controle. Por exemplo, sua aplicação usa o pacote fs? Se o fs se comporta estranho, você mexe no fonte dele, escreve testes e commita no repo? Até poderia, já que o Node.js é 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 mexer no sistema de arquivos.

O mesmo vale para o seu banco de dados e APIs externas. Sua aplicação depende deles para funcionar, mas você não gerencia o código deles, apenas usa, geralmente através de pacotes que, apesar de serem open-source muitas vezes, você não gerencia com frequência também.

Esses pacotes e recursos não-gerenciáveis por você, cujo código você usa mas não mexe, são os 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.

Tentando trazer de uma maneira bem prática, se você se focar em mockar módulos da node_modules ao invés de seus módulos, você estará atacando o mal das dependências na raiz e ao mesmo tempo poderá seguir buscando uma alta cobertura de testes no SEU código, pois o código da node_modules não entra no coverage report.

Para ajudar a tornar esta explicação mais convincente, vou trazer um case que acho que pode ajudar: mocks do Prisma.

Curso Node.js e MongoDB

Preparando os Testes

Quando trabalhamos com bancos SQL é muito comum o uso de ORMs, que são bibliotecas que nos facilitam muito a vida mapeando as tabelas do banco em objetos na nossa aplicação. Em NestJS, um dos ORMs mais popular é o Prisma, que já ensinei a usar aqui no blog antes.

Ele possui suporte a vários bancos diferentes e você cria e manipula os schemas e dados de maneira completamente agnóstica de vendor. O exemplo abaixo é de definição de uma tabela de users, por exemplo.

Sendo que este schema é usado pelo UserService.ts  abaixo:

Não precisa se preocupar em ter um banco de dados de fato, pois justamente é isso o que não queremos, depender de um banco para fazer nossos testes.

Seu projeto deve ter uma pasta test com alguns arquivos dentro, certo? Livre-se deles bem como de um app.controller.spec.ts pois vamos criar nossos próprios testes. Agora dentro de test, crie um arquivo user.service.spec.ts, com o seguinte conteúdo:

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 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 service 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).

Para testar, basta rodar o comando npm test no terminal e terá um sucesso como resultado se tudo foi implementado corretamente.

Mas claro, esse nosso teste inicial é bem fraquinho, vamos para um cenário real que usaria o Prisma e consequentemente o banco de dados, como abaixo.

O que acontece se eu rodar este teste?

Se eu tiver configurado o .env corretamente, se eu tiver criado o banco corretamente e se nele tiver uma tabela users com um registro de id 1, ele deve passar com sucesso. No entanto, se qualquer uma destas variáveis der uma leve oscilada, ele não vai funcionar. Agora escale isso mentalmente para dezenas ou centenas de testes (algo normal em um projeto de médio porte) necessitando de vários bancos, várias tabelas e de registros específicos nelas para funcionar.

Certo, isso é a parte mais simples e óbvia. Temos que mockar este recurso de banco de dados, certo?

Como mockar o Prisma?

Se você pegar o que ensinei no último tutorial e não pensar muito a respeito, você pode criar funções que mockam as funções do prisma, manualmente, uma a uma, certo?

No entanto, este processo é muito trabalhoso e por ser manual, pode ser falho. Além disso, se você não cuidar e mockar corretamente corre o risco de mockar o seu código por engano e isso não é legal. Para mockar exclusivamente o Prisma e de maneira muito assertiva, você pode usar o pacote Jest Mock Extended que facilita bastante esta atividade. Rode o comando abaixo para fazê-lo:

Agora certifique-se de você possui um db.ts dentro de src, que inicializa e exporta o objeto do prisma client (coloquei a diretiva istanbul para ser ignorado na contabilização de code coverage):

Com o db.ts preparado, vamos criar a versão mockada dele, em um db.mock.ts dentro da pasta test.

Aqui carregamos as dependências do Prisma e do Jest Mock Extended. Usamos a função de mock nativa do Jest para mockar o módulo db, substituindo-o pelo uso da nova função mockDeep, que faz uma “cópia” mockada do Prisma Client, substituindo o módulo db original. Também definimos via beforeEach que antes de cada teste, vamos reiniciar nosso mock do zero, para ele estar sempre limpo. Mock este que estamos exportando sob nome de prismaMock mais abaixo.

Aproveitei e já incluí um export para um usuário mockado mais ao final, iremos precisar de um.

Agora como podemos usar estes prismaMock e userMock em nossos testes? Importe-os normalmente no topo do user.service.spec.ts e depois use-os no seu teste que antes estava dando erro pela falta de banco:

No código acima, a única adição foi a segunda linha do teste, onde chamamos o prismaMock alterando a sua função findUnique para que resolva um userMock pré-definido. Por que a findUnique? Porque ela é usada internamente pelo getUser mais abaixo, assim, ao invés do Prisma ir no banco quando for pegar o usuário, ele vai pegar nosso objeto user mockado.

Repare que usei mockResolvedValue pois a função em questão retorna uma promise com um objeto dentro. Existem outras alternativas de funções de mock que você pode usar se quiser.

Antes de rodar o teste, você precisa fazer um ajuste no seu tsconfig.json, na propriedade strictNullChecks, mudando-a para true ou terá erros de tipagem (erros do TS na verdade, que se perde com o mock deep). Ao rodar, pode inclusive manter seu banco de dados desligado, pois não estaremos usando ele.

Agora, ao escrever seus demais testes, basta sempre usar o prismaMock antes da função do service, mockando a função (ou as funções) que serão usadas internamente e retornando um objeto mockado que sirva a seu teste. Se quiser rodar os testes com métricas de cobertura de código, basta usar npm run test:cov ao invés do npm test comum.

Você ainda pode querer testes integrados, mas em menor volume, bem como ainda pode querer testes manuais, em volume menor ainda, mas o seu código, esse sim você deve ter um grande volume e rodar muitas e muitas vezes a cada alteração no projeto, adição de funcionalidades, refatorações, antes de deploy, etc.

E para conseguir isso sem ter uma série de problemas, você precisa aplicar técnicas como mocking corretamente. Por isso escrevi este artigo. 🙂

Espero ter ajudado.

Um abraço e sucesso.

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 *