Teste unitário com Jest

Jest é um framework de teste unitário de código aberto em JavaScript criado pelo Facebook a partir do framework Jasmine. Jest é uma das ferramentas de teste unitário mais difundidas dentro da comunidade de JavaScript.

Jest é um framework de teste unitário de código aberto em JavaScript criado pelo Facebook a partir do framework Jasmine. Jest é uma das ferramentas de teste unitário mais difundidas dentro da comunidade de JavaScript.

Visão Geral

O Jest foi inicialmente criado para testar o framework React, também criado pelo Facebook. Porém sua implementação se tornou muito mais ampla, sendo utilizado como ferramenta de teste unitário para diversas plataformas JavaScript como Node e Redux, e até mesmo plataformas em TypeScript como Angular e Ionic.

Por exemplo, quando precisamos testar se uma função de consulta está se comunicando devidamente com uma API, o mais comum é executar essa função e exibir o resultado no console:

let cliente_dao = new ClienteDAO() cliente = cliente_dao.find_by_id(10) console.log(cliente.nome) //o resultado esperado é "Edson Arantes do Nascimento"

Em um projeto maior esse tipo de teste tende a se tornar inviável e inseguro a medida que a complexidade do módulo e da unidade aumentam. É natural sentir falta de uma metodologia de testes, que permita medir a cobertura de teste do código, tornando fácil criar os cenários nos quais as falhas serão percebidas. Os testes unitários suprem essas e outras carências do processo de teste de código.

test('Busca o nome do cliente pelo id', () => { let cliente_dao = new ClienteDAO() cliente = cliente_dao.find_by_id(10) expect(cliente.nome).toBe("Edson Arantes do Nascimento") })

O programador JavaScript conta com uma ferramenta muito poderosa e flexível para implementar testes unitários em seus projetos. Até a data dessa documentação o Jest está na versão 23.3. Jest é compatível com o EcmaScript 5 em diante

Instalação

O Jest pode ser instalado pelo Yarn ou pelo NPM como uma dependência externa.

yarn

Para adicionar o Jest ao seu projeto basta rodar o comando.

yarn add --dev jest

npm

Para adicionar o Jest ao seu projeto basta rodar o comando:

npm install --save-dev jest

Configuração

Sim, o Jest utiliza o próprio gerenciador de pacotes usado em sua instalação para executar os testes através do comando test, para isso é necessário adicionar ao documento package.json a seguinte sessão:

{ "scripts": { "test": "jest" } }

Escrevendo Testes

Nesta seção veremos como escrever diversos testes com o Jest, a fim de aprender como lidar com essa ferramenta em diferentes cenários.

Visão geral

Para um primeiro exemplo vamos testar a função freteGratis dentro do módulo descontos.js:

function freteGratis (valor) { return valor >= 150 }

Para testá-lo vamos criar o arquivo descontos.test.js, dentro desse arquivo iremos chamar a função test que será reconhecida pelo Jest como um teste efetivamente, como segundo argumento de test passamos uma função anônima sem argumentos, e nessa função executamos a função expect, e é essa função que irá verificar o resultado do código sendo testado:

const freteGratis = require('./descontos').freteGratis() test('freteGratis é verdadeiro para 200', () => { expect(freteGratis(200)).toBeTruthy() })
Gratis = require('./descontos').freteGratis() test('freteGratis é verdadeiro para 200', () => { expect(freteGratis(200)).toBeTruthy() })

Em seguida rodamos o seguinte comando no terminal para executar o teste:

npm test

Ou então:

yarn test

Na linha de comando o Jest irá notificar se o teste passou ou se falhou.

"descontos" PASS test/descontos.test.js OK freteGratis é verdadeiro para 200 (5ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.814s Ran all test suites matching /descontos/i.

Na prática

Jest utiliza de "matchers" (combinadores) para realizar os testes efetivamente. Existem diversos matchers para cada situação em particular dentro do contexto de testes. Os matchers são implementados a partir da chamada de expect() seguinte a sintaxe:

test('descrição do teste', () => { expect("valor esperado").toMatch("código testado"); });

Exemplo 1

O mais comum em teste unitário costuma ser o teste de igualdade. O Jest possui matchers de comparação que independem do tipo, estes são:

A função .toBe(valor) testa se o valor passado é idêntico ao esperado em valor e tipo .

test("resultados devem ser idênticos", () => { let geladeira = produto.findGeladeiraById(12) expect(geladeira.modelo).toBe('Eletrolux') })

A função .toEqual(valor) testa recursivamente cada valor do objeto ou array.

test("resultados devem possuir os mesmo atributos", () => { let geladeira = produto.findGeladeiraById(12) expect(geladeira).toEqual({preco: 1249.99, ano: '2017', modelo: 'Eletrolux'}) })

Para cada matcher de comparação é possível usar o .not para fazer uma comparação oposta.

test("resultados não devem possuir os mesmo atributos", () => { let geladeira = produto.findGeladeiraById(12) expect(geladeira).not.toEqual({preco: 1249.00, ano: '2014', modelo: 'Brastemp'}) })

Exemplo 2

A função toBeNull Testa se o resultado passado tem valor igual a null.

var id = "56e6dd2eb4494ed008d595bd" test('resultado precisa ser null', () => { expect(UserModel.findById(id, user => user.toObject() )).toBeNull() })

A função toBeUndefined testa se o resultado passado tem valor igual a undefined.

test('resultado precisa ser undefined', () => { expect(UserModel.findById(id, user => user.toObject() )).toBeUndefined() })

A função toBeDefined testa se o resultado passado não tem valor igual a undefined.

test('resultado não pode ser undefined', () => { expect(UserModel.findById(id, user => user.toObject() )).toBeDefined() })

A função toBeTruthy testa se o resultado passado tem valor que pode ser passado como true em um if.

test('resultado precisa ser true', () => { expect(UserModel.findById(id, user => user.toObject() )).toBeTruthy() })

A função toBeFalsy testa se o resultado passado tem valor que pode ser passado como false em um if.

test('resultado precisa ser false', () => { expect(UserModel.findById(id, user => user.toObject() )).toBeFalsy() })

Exemplo 3

Matchers usados para fazer comparações numéricas:

A função toBeGreaterThan testa se o resultado passado é maior que o esperado.

test('Dolar deve ser maior que Real', () => { expect(moedas.getDolar()).toBeGreaterThan(moedas.getReal()) })

A função toBeGreaterThanOrEqual testa se o resultado passado é maior ou igual ao esperado.

test('salario deve ser maior ou igual ao salário mínimo', () => { expect(funcionario.getSalario()).toBeGreaterThanOrEqual(api.salarioMinimo()) })

A função toBeLessThan testa se o resultado passado é menor que o esperado.

test('Peso deve ser menor que Libra', () => { expect(moedas.getPeso()).toBeLessThan(moedas.getLibra()) })

A função toBeLessThanOrEqual testa se o resultado passado é menor ou igual ao esperado.

test('salario de não comissionado deve ser menor ou igual ao salário de comissionado', () => { expect(naoComissionado.getSalario()).toBeLessThanOrEqual(comissionado.getSalario()) })

Nota: Os matchers toBe e toEqual são usados para testar equidade numérica.

Exemplo 4

É possível verificar se uma certa string está dentro de uma expressão maior com o comando toMatch

test("Uma palavra dentro de outra", () => { expect('Inconstitucionalissimamente').toMatch(/constitucional/) })

Exemplo 5

É possível verificar se um array possui um elemento em específico com toContain.

const verbosHttp = [ 'GET', 'POST', 'PUT', 'DELETE' ] test('O verbo PUT está na lista' ,()=>{ expect(verbosHttp).toContain('PUT') })

Exemplo 6

Para testar se uma função em particular gera uma exceção basta usar toThrow.

function conectaBancoDeDados(){ throw new Error('Não foi possível conectar ao banco de dados') } test('conexão com o banco de dados falha como esperado' ,()=>{ expect(conectaBancoDeDados).toThrow(Error) })

Também é possível verificar a mensagem de erro, como demonstrado abaixo.

function conectaBancoDeDados(){ throw new Error('Não foi possível conectar ao banco de dados') } test('conexão com o banco de dados falha como esperado' ,()=>{ expect(conectaBancoDeDados).toThrow('Não foi possível conectar ao banco de dados') expect(conectaBancoDeDados).toThrow(/dados/) })

Funções de Mock

Visão geral

Funções de mock são funções que permitem criar módulos e funções falsas utilizadas para simular uma dependência. Elas facilitam testar as ligações no código por substituir uma dependência cuja implementação seria inviável dentro do teste. O ato de mockar uma função torna possível capturar chamadas dessa função (e seus parâmetros) pelo código sendo testado, permite capturar instâncias de funções construtoras quando implementadas usando new e também permitem a configuração dos valores retornados para o código sob teste.

Funções mock são criadas a partir da função jest.fn() e podem ser configuradas para reproduzir o comportamento desejado.

Na prática

Exemplo 1

Para ilustrar uma função mock vamos tomar de exemplo a função pagamentoMoedaEstrangeira que converte o valor da moeda de real para a desejada de acordo com o valor atualizado provido em uma API que é injetada na função, a API retorna o valor da moeda em relação ao real de acordo com o parâmetro passado:

function pagamentoMoedaEstrangeira (tipoMoeda, valor, currency) { if (tipoMoeda === Currency.QUOTACAO_DOLAR) { valor *= currency.getQuotacaoDolar() } else if (tipoMoeda === Currency.QUOTACAO_EURO) { valor *= currency.getQuotacaoEuro() } else if (tipoMoeda === Currency.QUOTACAO_LIBRA) { valor *= currency.getQuotacaoLibra() } else { throw Error('moeda não disponível') } return valor } module.exports = { pagamentoMoedaEstrangeira }

Então conseguimos simular uma dependência do código sob teste sem aumentar muito a complexidade do código de teste.

const { pagamentoMoedaEstrangeira } = require('../src/operacoes.js') const mockCurrency = {} mockCurrency.getQuotacaoDolar = jest.fn() mockCurrency.getQuotacaoDolar.mockReturnValue(3) test('chamar getQuotacaoDolar uma vez', () => { expect(pagamentoMoedaEstrangeira('dolar', 300, mockCurrency)).toBe(900) })

Exemplo 2

Todas as funções mock possuem a propriedade especial .mock, a qual armazena dados sobre como a função foi chamada e o que ela retornou.

Usando a função pagamentoMoedaEstrangeira como alvo do teste veja o exemplo:

test('chamar getCurrency uma vez', () => { expect(pagamentoMoedaEstrangeira('dolar', 300, mockCurrency)).toBe(900) // verifica se a função foi chamada 1 vez expect(mockCurrency.getCurrency.mock.calls.length).toBe(1) // verifica se o primeiro argumento passado na primeira chamada foi zero expect(mockCurrency.getCurrency.mock.calls[0][0]).toBe(0) // verifica se o resultado da primeira chamada foi 3 expect(mockCurrency.getCurrency.mock.results[0].value).toBe(3) })

A propriedade .mock também registra o valor de this para cada chamada.

A propriedade .calls de .mock possui uma matriz com as informações de quantas vezes o mock foi chamado e quais atributos foram passados em cada chamada.

A sintaxe utilizada no exemplo anterior é a seguinte:

funcaoMock.mock.calls[call][arg]

Onde call é a vez em que a função foi chamada e arg é o parâmetro que foi passado naquela chamada.

Exemplo 3

Funções mock podem ser configuradas para injetar valores de teste dentro do código sendo executado durante o teste:

const falso = jest.fn() falso .mockReturnValueOnce('primeiro') .mockReturnValueOnce('segundo') .mockReturnValue('mais de uma vez') console.log(falso(), falso(), falso(), falso()) // > 'primeiro', 'segundo', 'mais de uma vez', 'mais de uma vez'
Os valores de retorno de uma função mock são definidos pelos métodos .mockReturnValue() e .mockReturnValueOnce(), com a diferença que o segundo retorno só será chamado uma vez. Métodos de retorno de valor irão seguir em execução a ordem com a qual foram declaradas.

Exemplo 4

Em certas situações é necessário passar um módulo como dependência para uma função sendo testada mas não queremos que ela de fato execute essa dependência toda vez que o teste for feito, como no caso de testar uma função que interage com uma API por exemplo.

Nestes casos é possível criar um módulo mock bem como definir resultados para funções desse módulo. A sintaxe para mockar um módulo e definir uma função com retorno é:

const { modulo } = require('arquivo') jest.mock('modulo') modulo.funcao.mockResolvedValue("qualquer tipo de retorno")

Abaixo vemos um exemplo com uma função de busca de usuário enviando um id para uma API e retorna o usuário encontrado:

const { api } = require('../helpers/usuarioAPI') const buscarUsuario = id => api.encontrarPorId(id).then(resp => resp.data) const usuarioRepository = { buscarUsuario: buscarUsuario // ... outras funções } module.exports = { usuarioRepository }

Para testar esse método sem contatar diretamente a API (e portanto evitando um teste com uma dependência externa) usamos a função jest.mock() para automaticamente mockar o módulo usuarioRepository.

Em seguida declaramos um mockResolvedValue na função encontrarPorId para que retorne sempre o mesmo valor quando for executada pela função que estamos testando, nesse caso a função buscarUsuario:

const { api } = require('../helpers/usuarioAPI') const { usuarioRepository } = require('../src/usuarioRepository') jest.mock('../helpers/usuarioAPI') test('qualquer coisa', () => { const resposta = {data: {id: 1, nome: 'José', idade: 42}} api.encontrarPorId.mockResolvedValue(resposta) usuarioRepository.buscarUsuario(12).then( usuario => expect(usuario).toEqual(resposta.data) ) })

Na Linha 1 requirimos o módulo usuarioAPI para a constante api. Na Linha 4 definimos que este módulo será substituído por um mock quando implementado dentro do contexto de teste. Na Linha 8 definimos um valor de retorno para quando entrarPorId resolver.

Exemplo 5

Existem casos em que é necessário ir além de especificar valores de retorno, sendo preciso substituir completamente a implementação da função mock. Isso pode se feito através de jest.fn:

UserModel.createUser = jest.fn( parametros => { console.log('um mock substituiu uma implementacao') return parametros })

Com isso podemos simular comportamentos complexos de uma dependência e realizar testes outrora inviáveis.

Outra forma de fazer mockar uma implementação é através do método mockImplementation em uma função mock:

UserModel.createUser.mockImplementacion( parametros => { console.log('um mock substituiu uma implementacao') return parametros })

Caso seja necessário recriar uma função sendo chamada múltiplas vezes e produzir resultados diferentes, use mockImplementationOnce:

const UserModel.createUser = jest .fn() .mockImplementationOnce(param => param) .mockImplementationOnce(param => {})

Testes de Código Assíncrono

Quando há código rodando de forma assíncrona dentro de um teste, o Jest precisa saber quando o código termina de ser testado antes de poder começar o próximo teste. Jest possui diversas formas de fazer isso.

Visão geral

Por padrão os testes em Jest completam assim que chegam ao fim de sua execução, ou seja, se uma função chamar uma callback, o teste completará ao fim da execução de dita função, antes mesmo de chamar a callback.

O exemplo abaixo não funcionará:

function buscarResultado (param) { return param('resultado') } test('callback da forma errada', () => { const callback = (dados) => { expect(dados).toBe('resultado') } buscarResultado(callback) })

Para garantir que o teste seja feito de forma adequada basta passar done como argumento da callback ao invés de passar uma função com argumento vazio. Jest aguardará até que done seja chamado antes do fim de concluir o teste.

Abaixo a forma correta de testar callbacks:

function buscarResultado (param) { return param('resultado') } test('callback da forma certa', done => { const callback = (dados) => { expect(dados).toBe('resultado') done() } buscarResultado(callback) })

O código acima é praticamente idêntico ao anterior exceto na Linha 5 onde é passado done como parâmetro e na Linha 8 com sua chamada.

Na prática

Exemplo 1

Caso seu código possua promises, há uma forma mais simples de manejar testes assíncronos. Basta retornar uma promise de seu teste e o Jest irá aguardar que essa promise resolva. Caso a promise seja rejeitada o teste falhará.

Por exemplo, a função lerArquivo importa a função readFile e a transforma em uma promise após uma rápida validação:

const { readFile } = require('fs') const { promisify } = require('util') function lerArquivo (path, options = {}) { if (!path || typeof path !== 'string') { return Promise.reject(new Error('path is incorrect or not defined')) } return promisify(readFile)(path, options) } module.exports = { lerArquivo }

Para testá-la criamos um pequeno index.html dentro da pasta files apenas com "hello world" escrito. Criamos o teste para a função lerArquivo e implementamos um segundo .then com o nosso expect:

const { lerArquivo } = require('../helpers/arquivosManager') test('testando leitura de arquivo', () => { lerArquivo('./files/index.html') .then(buffer => buffer.toString('utf8')) .then(conteudo => expect(conteudo).toBe('hello world\n')) })

Na Linha 5 é feita a chamada da promise lerArquivo, na Linha 6 é feito a conversão do resultado da promise para UTF-8 e na Linha 7 o teste é feito com o resultado dessa conversão, que deve ser igual a 'hello world\n'.

Exemplo 2

Também é possível usar o matcher .resolves e o Jest irá esperar que a promise resolva. Se a promise for rejeitada o teste falhará.

const { lerArquivo } = require('../helpers/arquivosManager') test('lendo arquivo', () => { expect(lerArquivo('./files/index.html') .then(buffer => buffer.toString('utf8'))) .resolves.toEqual('hello world\n') })

Observe na Linha 5 que neste exemplo a promise é feita dentro do expect ao contrário do que foi feito no exemplo anterior. Na Linha 7 indicamos ao jest que a promise ira retornar 'hello world\n' quando resolver

Exemplo 5

Caso o esperado seja que a promise seja rejeitada basta usar o matcher .rejects. De forma análoga ao .resolves, se a promise resolver o teste falhará

const { lerArquivo } = require('../helpers/arquivosManager') test('lendo arquivo', () => { expect(lerArquivo('')).rejects.toBeDefined() })

Na Linha 4 é definido que a promise lerArquivo deve terminar em uma rejeição ou o teste irá falhar.

Artigos relacionados