Conteúdo:
- Conceitos e fundamentos de testes;
- O que é a cultura de testes;
- testes estáticos e unitários;
- asserções;
- frameworks de testes jest;
- testes de integração com supertest.
Os testes são importantes para evitar descobrir erros durante o runtime ou tempo de execução.
- E2E(end-to-end): topo da pirâmide, testa a aplicação de ponta a ponta
- Integração: testes de integração entre as partes do projeto
- Unitários: base da pirâmide, menores componentes testados individualmente
Quanto mais ao topo da pirâmide mais integração, maior consumo de recursos e mais complexo e devagar. Quanto mais a base da pirâmide mais isolado, maior volume de testes e mais simples e rápido.
- Testes curtos e isolados;
- Análise de funções ou métodos;
- Não garante uma integração de módulos.
Exemplo: testar a função que calcula as taxas de um método de pagamento.
- Testes de rotas e requisições;
- Comunicação dos módulos.
- Não analisa todo o fluxo da aplicação.
Exemplo: testar a requisição das rotas de uma API.
- Ponta a ponta, alto nível;
- Testes longos e completos;
- Análise de todos os módulos e stacks. Ex: front-end/banco de dados/microsserviços.
- Tempo longo e custo alto;
- Complexa estrutura de testes.
Exemplo: testar uma aplicações web desde o cadastro, cliques e etc, ou seja, todas as funcionalidades.
Um ambiente onde a equipe de desenvolvimento pode implementar e gerir os testes.
Fatores fundamentais:
- Qualidade;
- Confiança;
- Tempo.
Fases do teste:
- Análise de requisitos: identificar as funcionalidades que serão testadas;
- Plano de teste: elaborar um plano para definir quais ferramentas serão utilizadas, quem vai criar, o tempo, os gastos de recursos;
- Caso de teste: os testes em si, quais são entradas, saídas, todo o mapeamento;
- Ambiente de teste: onde e como esses testes serão feitos;
- Implementação: documentação dos resultados dos testes, detalhamentos dos problemas e melhorias.
Para analisar o código sem necessariamente executá-lo, verificando se algumas boas práticas e formatação padronizada foram adotadas na implementação.
Para realizar os testes estáticos estabelecendo uma padronização do código e identificação de erros.
Instalação: npm install --save-dev eslint
Configuração: npx eslint
√ Which framework does your project use? · none
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · node
√ How would you like to define a style for your project? · gu√ Where does your code run? · node
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · airbnb
√ What format do you want your config file to be in? · JSON Checking peerDependencies of eslint-config-airbnb-base@latest
The config that you've selected requires the following dependencies:
eslint-config-airbnb-base@latest eslint@^7.32.0 || ^8.2.0 eslint-plugin-import@^2.25.2 √ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-config-airbnb-base@latest, eslint@^7.32.0 || ^8.2.0, eslint-plugin-import@^2.25.2
Execução: npx eslint <nome do arquivo>
Framework de testes em javascript. É utilizado como uma dependência de desenvolvimento.
Instalação: npm install --save-dev jest
Para executar o jest com a importação de módulos deve-se usar uma flag --experimental-vm-modules. Atualizando os scripts no package.json:
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --watch",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage"
},Para executar o comando de teste: npm run test ou npm test
Opcionalmente pode adicionar o :watch e :coverage.
Para configurar parâmetros do jest deve-se criar um arquivo chamado jest.config.js.
Na nomeação dos arquivos de testes existe algumas convenções. Os arquivos podem estar dentro de uma pasta chamada test ou _test_ ou dentro da pasta de origem do arquivo
a ser testado.
Se você deseja escrever um teste unitário para cobrir, por exemplo, um arquivo
editorasController.js, o arquivo de teste deve ser especificado com o sufixoeditorasController.test.jsoueditorasController.spec.js, pois é uma convenção de mercado. Há ainda quem prefira especificar o tipo de teste comnomeDoArquivo.unit.test.jsenomeDoArquivo.int.test.jspara informar se o teste é unitário ou de integração. Todas estas formas são reconhecidas pelo Jest.
O modo watch acompanha simultaneamente os arquivos de testes, de modo que ao realizar alterações e salvar, os testes são executados novamente em tempo real.
Execução: npm run test:watch
O modo coverage gera um relatório dos testes com mais informações.
Execução: npm run test:coverage
| Parâmetro | Descrição |
|---|---|
% Stmts |
São statements, ou afirmações, em percentual, que foram percorridas. |
% Branch |
São as bifurcações de condicionais ou laços de repetição. |
% Func |
Quais funções foram chamadas dentro daquela base de códigos. |
% Lines |
Percentual de linhas que foram percorridas. |
Dentro do projeto é criada uma pasta chamada coverage onde nela dentro da pasta lcov-report há um arquivo html que mostra as mesmas informações da tabela, mas
de forma mais simples de navegar e com a possibilidade de visualizar os arquivos.
O jest usa matchers para testar valores de maneiras diferentes.
test('dois mais dois é quatro', () => {
expect(2 + 2).toBe(4);
});Nesse código, o .toBe(4) é o "matcher".
Mais exemplos de matchers:
| Método | Descrição |
|---|---|
expect(value) |
Testar um valor. |
expect.extend(matchers) |
Adicionar seus próprios "matchers". |
expect.anything() |
Corresponde a qualquer coisa menos null e Undefined |
expect.any(constructor) |
Testa qualquer coisa que é criada com um construtor. |
expect.arrayContaining(array) |
O array esperado é um subconjunto do array recebido. |
expect.assertions(número) |
Verifica que um certo número de verificações são chamadas durante um teste. |
expect.closeTo(number, numDigits?) |
é útil quando você compara números quebrados num array. |
expect.hasAssertions() |
Verifica que pelo menos uma verificação é chamada durante um teste. |
expect.not.arrayContaining(array) |
Quando o array esperado não é um subconjunto do array recebido. |
expect.not.objectContaining(object) |
Quando o objeto esperado não é um subconjunto do objeto recebido. |
expect.not.stringContaining(string) |
Quando o valor recebido não é uma String ou não corresponde ao valor esperado da String. |
expect.not.stringMatching(string / regexp) |
Quando o valor recebido não é String ou não corresponde a String esperada ou a expressão regular. |
expect.objectContaining(object) |
Corresponde a qualquer objeto recebido que recursivamente coincide com as propriedades esperadas. |
expect.stringContaining(string) |
Quando o valor recebido é uma String que contém a String esperada. |
expect.stringMatching(string / regexp) |
Quando o valor recebido é uma String que contém a String ou expressão regular esperada. |
expect.addSnapshotSerializer(serializer) |
Para adicionar um módulo que formata estruturas de dados específicas da aplicação. |
.not |
Se você sabe como testar algo, .not permite que você teste seu oposto. |
.resolves |
Decodifica o valor de uma promessa cumprida, para que qualquer outro matcher possa então ser encadeado. |
.rejects |
Decodifica o motivo de uma promessa rejeitada, para que qualquer outro matcher possa ser encadeado. |
Ao testar uma API é importante ter a separação do app.js e do server.js para que os testes consigam ser executados corretamente.
Na pasta de tests foi replicada a estrutura do src: a pasta models criada dentro da pasta de tests e os arquivos de testes de cada entidade, mas poderia ter sido organizado de outra maneira. Exemplo:
import Editora from '../../models/editora.js'
describe('Testando o modelo Editora', () => {
const objetoEditora = {
nome: 'CDC',
cidade: 'Rio de Janeiro',
email: 'c@c.com'
};
it('Deve instanciar uma nova editora', () => {
const editora = new Editora(objetoEditora);
expect(editora).toEqual(
expect.objectContaining(objetoEditora),
);
});
it.skip('Deve salvar editora no banco de dados', () => {
const editora = new Editora(objetoEditora);
editora.salvar().then((dados) => {
expect(dados.nome).toBe('CDC');
});
});
it('Deve salvar no banco de dados usando a sintaxe moderna', async () => {
const editora = new Editora(objetoEditora);
const dados = await editora.salvar();
const retornado = await Editora.pegarPeloId(dados.id);
expect(retornado).toEqual(
expect.objectContaining({
id: expect.any(Number),
...objetoEditora,
created_at: expect.any(String),
updated_at: expect.any(String),
})
);
});
});O método .skip é utilizado para pular um teste espcífico ao executar os testes.
Quando os testes são executados o ideal é que não sejam criados novos registros no banco de dados utilizado em produção, e sim que exista um ambiente de desenvolvimento de testes para que os testes possam ser desenvolvidos sem conflitos e alteração no banco de dados.
O jest fornece um mock de funções através do método jest.fn(). Para utilizar deve importar o jest do pacote @jest/globals.
import { jest } from '@jest/globals';
it('Deve fazer uma chamada simulada ao BD', () => {
const editora = new Editora(objetoEditora);
editora.salvar = jest.fn().mockReturnValue({
id: 10,
nome: 'CDC',
cidade: 'Sao Paulo',
email: 'c@c.com',
created_at: '2022-10-01',
updated_at: '2022-10-01',
});
const retorno = editora.salvar();
expect(retorno).toEqual(
expect.objectContaining({
id: expect.any(Number),
...objetoEditora,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
);
});
});O ambiente de testes serve para verificar todos os componentes de um sistema nas condições mais próximas possíveis das condições de uso real pelos usuários, para que possíveis bugs e erros de implementação possam ser corrigidos antes que o programa ou funcionalidade seja disponibilizado - o tal “ambiente de produção”.
Diferente do ambiente de desenvolvimento que é onde a aplicação é desenvolvida - a partir do zero, novas funcionalidades ou atualizações - e onde já podem ser feitos testes unitários e de integração, o ambiente de testes é onde são feitos os testes mais complexos e que levam mais tempo para serem implementados, como testar a integração com outras partes do sistema, comportamento com o banco, performance das tarefas, etc. Normalmente os QAs(ou testers) são responsáveis por isso.
Hooks são utilizados quando queremos dar ao programa um comportamento específico em alguma determinada circunstância - por exemplo, antes, durante ou depois de determinado código ser executado.
Muitas vezes ao escrever testes você tem algum trabalho de configuração que precisa acontecer antes de executar testes, e você tem algum trabalho de acabamento que precisa acontecer após os testes executarem. Jest fornece funções de auxilio para lidar com isso.
Exemplo com beforeEach() e afterEach() que são utilizados para executar algo antes e depois de cada teste respectivamente.
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});Testes de integração são responsáveis por testar a integração entre dois módulos - ou partes - diferentes da aplicação. Testar os endpoints de uma API é um tipo de teste de integração.
Faz asserções HTTP facilitando o teste de rotas de uma API REST. É instalada como uma dependência de desenvolvimento com o comando npm install --save-dev supertest.
Para utilizar deve-se importar o request no arquivo de teste. Com a sintaxe de módulos do ES6+:
import request from 'supertest';Exemplo rota GET:
describe('GET em /editoras', () => {
it('Deve retornar uma lista de editoras', async () => {
const resposta = await request(app)
.get('/editoras')
.set('Accept', 'application/json')
.expect('content-type', /json/)
.expect(200);
expect(resposta.body[0].email).toEqual('e@e.com');
});
});Utilizando o método .each é possível realizar um "loop de testes" onde mais de um parâmetro é testado, ou seja, pega cada elemento do array e vai testar cada um separadamente. Na rota de PUT, por exemplo, podemos testar a atualização de mais de um campo. Exemplo:
describe('PUT em /editoras/id', () => {
test.each([
['nome', { nome: 'Casa do Código' }],
['cidade', { cidade: 'RJ' }],
['email', { email: 'cdc@cdc.com '}],
])('Deve alterar o campo %s', async (chave, params) => {
await request(app)
.put(`/editoras/${idResposta}`)
.send(params)
.expect(204);
});
});Os objetos com os campos que serão atualizado são passados como parâmetros na função de teste como params e a string antes dos objetos, que indicam os nomes dos testes, é passada como parâmetro chave e o %s (indicado na documentação do Supertest) na descrição do teste.
Com a função --verbose do jest é possível receber a descrição de cada teste executado.
O método jest.spyON é uma função mock do Objeto Jest utilizada para obter mais informações sobre as chamadas de um determinado método. Exemplo: se o método está sendo chamado.
describe('PUT em /editoras/id', () => {
test.each([
['nome', { nome: 'Casa do Código' }],
['cidade', { cidade: 'RJ' }],
['email', { email: 'cdc@cdc.com '}],
])('Deve alterar o campo %s', async (chave, params) => {
const requisicao = { request };
const spy = jest.spyOn(requisicao, 'request')
await requisicao.request(app)
.put(`/editoras/${idResposta}`)
.send(params)
.expect(204);
expect(spy).toHaveBeenCalled();
});
});


