TDD: Como criar mocks de BD em Node.js com Jest e Prisma

Node.js

TDD: Como criar mocks de BD em Node.js com Jest e Prisma

Luiz Duarte
Escrito por Luiz Duarte em 11/01/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 TDD, com a suíte de testes Jest, para aplicações Node.js. 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 acima.

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!

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

Estruturando os Testes

Quando trabalhamos com bancos de dados é 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 Node.js, um ORM muito popular é o Prisma, que já ensinei a usar aqui no blog antes.

Ele possui suporte a vários bancos de dados diferentes e você cria e manipula os schemas e dados de maneira completamente agnóstica de vendor. O exemplo abaixo é o schema.prisma de uma coleção de customers em um banco MongoDB, por exemplo.

Sendo que o Prisma Client é inicializado em um arquivo src/client.ts como abaixo:

Uma coisa muito comum é a gente não manipular diretamente o Prisma na aplicação, mas ao invés disso, criamos um módulo de repositório, como abaixo (src/customersRepository.ts):

Para que possamos testar essas funções vamos escrever um primeiro teste. Vamos criar um arquivo src/customersRepository.spec.ts, que é um padrão de nomenclatura para arquivos de testes do Jest. Dentro dela, vamos colocar o mesmo teste que estava informalmente no index.ts:

Para que esse teste funcione, você vai precisar instalar as seguintes bibliotecas:

  • @prisma/client
  • prisma
  • ts-node
  • typescript
  • jest
  • @types/jest
  • ts-jest

E vai precisar também rodar o comando de inicialização do Jest:

Pode responder as perguntas default para todas perguntas, e depois ajuste o seu jest.config.js para que fique como abaixo:

O que acontece se eu rodar este teste com “npx jest”?

Se eu tiver configurado o .env corretamente, se eu tiver criado o banco corretamente no MongoDB e se nele tiver uma coleção customers com alguns registros, 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 coleções 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, a primeira coisa que você tende a fazer é criar uma pasta __mocks__ dentro de src e colocar um clienteRepository.ts dentro dela, mockando a função getCustomers, certo?

No entanto, se você fizer isso, você não estará testando o MongoDB (ok), nem o Prisma (ok) e nem o seu customersRepository (não ok), sua cobertura de teste ficará baixíssima. E isso não é legal. O SEU código DEVE estar sendo testado e com uma boa cobertura de testes ainda por cima (>80%). Sendo assim, você NÃO deve mockar ele, mas sim, mockar o código DOS OUTROS, como o código interno do Prisma, por exemplo.

Para isso, você pode usar o pacote Jest Mock Extended que facilita bastante esta atividade. Com ele, você cria os mocks do seu schema Prisma de forma praticamente automática. Então comece instalando este pacote:

Depois, crie um arquivo src/singleton.ts que será responsável por instanciar o mock do Prisma Client, com o seguinte conteúdo:

E por fim, no seu jest.config, inclua mais esta instrução nas configurações, que serve para indicar ao Jest que deve usar esse singleton.ts antes dos testes.

Mas e agora, o que muda em nossos testes?

O mesmo teste de antes deve ficar assim, usando os mocks:

Começamos importando o prismaMock pois usaremos ele para mockar o Prisma no interior dos testes, além de todas functions do repository, pois escreveremos testes para todas elas. Depois, tomei a liberdade de criar um customer “global” que será usado por todos testes.

Indo para o teste em si, repare como antes de chamar a getCustomers do repository eu uso o prismaMock, entrando na coleção de customers e sobrescrevendo a função findMany (usada internamente pelo getCustomers) com mockResolvedValue que neste teste sempre retornará um array com o customer de exemplo dentro.

Assim, esse teste irá funcionar independente das suas variáveis de ambiente para o Prisma, independente das suas configurações do banco de dados e independente dos registros existentes na coleção de customers. Ele é um teste unitário da função getCustomers do customersRepository. Ponto.

Rodando os testes agora, você manterá uma cobertura de teste alta sobre o SEU código. O código do Prisma, esse sim não estará sendo testado, ou sequer a infraestrutura do MongoDB, mas isso não é responsabilidade dos testes unitários, certo?

Vamos para um segundo teste, de getCustomer:

Novamente, repare como é praticamente igual ao primeiro, apenas mudando o retorno do mockResolvedValue de array para objeto e obviamente as chamadas e expects para tratar um único retorno.

Agora um terceiro teste, de adição de customer.

Não há grandes diferenças entre os testes usando mocks no Prisma. Basta você identificar qual a função do Prisma será necessário mockar (create neste caso) e mockar o seu retorno. O restante do teste em si é idêntico ao que seria sem mocking.

Experimente tentar escrever o update e delete sem olhar os fontes abaixo.

E agora rode com o comando “npx jest –coverage” para ter essa belíssima visão de cobertura de código.

Ué, você não conseguiu um 100% geral? Isso provavelmente porque o Jest está considerando que você deveria ter testes escritos para o client.ts e index.ts, certo? Só que ele está errado, então inclua a seguinte linha no topo de cada um dos arquivos que devem ser ignorados pelo Jest.

E voilá, você deve chegar no mesmo resultado que eu agora!

Você ainda pode querer testes integrados (end-to-end), 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 *