Test Runner

Um Test Runner (ou Test Framework) é uma ferramenta que executa testes automatizados em um projeto de software. Ele gerencia a execução dos testes, coleta os resultados e apresenta relatórios detalhados. É basicamente uma ferramenta que descobre, executa e organiza testes automatizados.

Existem algumas ferramentas no mercado como o jest que é um test runner para javascript, node, typscript etc. O pytest e o pytest-django que são feitos para python e django respctivamente, o proprio python também possui um test runner integrado o unittest.

Watch é quando o test runner monitora quaisquer alterações no código para rodar automaticamente os testes.

Setup é tudo o que precisa acontecer antes do teste começar.
Ex: criar conexão com banco, criar dados de exemplo, carregar arquivos etc.

Teardown é tudo que deve ser limpo ou finalizado após o teste acabar, mesmo que ele falhe.
Ex: fechar conexões, excluir arquivos, apagar registros de teste, etc.

TDD (Test-Driven Development), ou Desenvolvimento Orientado a Testes, é uma abordagem de programação onde os testes são escritos antes do código. Essa Abordagem segue o ciclo Red-Green-Refactor. Onde Red (Escreva um Teste Falhando) → Criar um teste para uma funcionalidade ainda inexistente. Green (Faça o Teste Passar) → Escrever o código mínimo necessário para passar no teste. Refactor (Refatorar) → Melhorar o código, garantindo que os testes ainda passem.

Uma abordagem interessante seria separar os tipos de testes para testes de unidades/arquivos que funcionam independente (unit), e arquivos que dependem de outras unidades para funcionarem (integration ou teste de serviço), como no exemplo abaixo:

End-to-end (E2E) São testes onde todo o fluxo do sistema é testado como um usuário real faria, garantindo que todas as partes (frontend, backend, banco de dados, etc.) funcionem juntas corretamente. Também conhecido como teste de UI (interface de usuário).

O esquema de pastas dos testes deve ser o clone do arquivo que se está testando:

Exemplo da estrutura de pasta de teste                                                                                                                                               Considerando essa estrutura

 

A ferramenta testing nativa do vscode (icone na barra lateral) permite  gerenciar, rodar e visualizar resultados de testes no seu código. Funciona integrando vários frameworks populares (pytest, jest, mocha, unittest, etc.). Permite você rodar testes, ver status (pass/fail), navegar direto para o teste, etc. caso tenha problemas com os testes não serem localizado apague as ultimas pastas do %APPDATA%\Code\User\workspaceStorage. Caso queira existem duas extensões para testes no vscode: Python Test Explorer for Visual Studio Code e uma universal chamada Test Explorer UI.


Jest

Geralmente usado para Javascript e TypeScript, também é muito usado com React, suporta snapshots e mocks. Mas não possui suporte ao ESM (ECMASCRIPT Modules). Comandos mais novos, como import, não funcionam no Jest, so os mais antigos como require.

Para criar scripts de testes coloque um nome e use a convenção nome.test.js exemplo: calculadora.test.js (o jest já localiza automaticamente os arquivos .test) O jest por padrão roda todos os testes em paralelo e não de forma serial (sequencial). então se um teste depender de dados que foram limpos por outro teste terá um problema. Para isso o jest dispõe de uma propriedade para roda-lo em modo sequencial (--runInBand - rodar de forma linear). 

É possivel escrever um arquivo de configuração para o jest, jest.config.js, nele pode conter variáveis de ambientes e etc.

Instalando o Jest

bash
$ # save-dev para instalar a dependência apenas para desenvolvimento
$ npm install --save-dev jest@29.6.2
package.json
"scripts": {
    "test": "jest --runInBand",
    "test:watch": "jest --watch --runInBand" // deixa automático
    //use "jest --watchAll" para monitorar tudo inclusive alterações que não foram "commitadas"
  },

Adicione o script de test ao arquivo package.json. O runInBand é para executar os testes de forma serial.

Executando o Jest

O comando para rodar o test por convenção é npm test

bash
$ npm test # Não precisa do npm run test, apenas se quiser
$ npm run test:watch # Deixa o test monitorando para salvar
$ npm run test:watch -- migrations # Executa apenas o teste de uma pasta, nesse caso na pasta migrations colocando migrations.get executa so o teste do método get (é um filtro)

| Exemplos Simples

exemplo.test.js
// Exemplo Simples
test("Nome do Teste", () => {
  console.log("entrou na arrow function teste");
});

// Exemplo com Expect
test("Nome do Teste", () => {
  expect(1).toBe(1);
});

arrow function é a forma mais ultilizada.

outras_formas.test.js
// Função declarada
test("Nome do Teste", callbackFunction);
function callbackFunction() {
  console.log("entrou no callback");
}

// Função anonima
test("Nome do Teste 2", function () {
  console.log("Entrou na função Anonima de teste");
});

Esse são formas menos utilizadas para usar a função nos argumentos dos testes, são elas função anonima e função declarada.

| Exemplo Calculadora

calculadora.test.js
// Importa o arquivo que será testado
const calculadora = require("../models/calculadora.js");
test("Função de Somar retorna 4", () => {
  const resultado = calculadora.somar(2, 2);
  expect(resultado).toBe(4);
});

Nesse exemplo, o arquivo em questão está dentro da pasta test do projeto.

calculadora.js
function somar(arg1, arg2) {
  return arg1 + arg2;
}
exports.somar = somar; // exporto a função

Nesse exemplo, o arquivo em questão está dentro da pasta models do projeto.

| Exemplo teste API

O arquivo de configuração database.js está no assunto relacionada postgresql.

A função async retorna uma Promise automaticamente, e em combinação com await que Pausa a execução até que a Promise seja resolvida.

fetch é uma função do JavaScript usada para fazer requisições HTTP (chamar APIs), como o curl.

get.test.js
test("Get to /api/v1/status should return 200", async () => {
  const response = await fetch("http://localhost:3000/api/v1/status");
  expect(response.status).toBe(200);
  // Parsing. Converte a resposta do servidor (texto puro) para um objeto JSON
  const responseBody = await response.json();
  expect(responseBody.updated_at).toBeDefined();

  //teste para verificar se o valor é uma data real
  const parseUpdatedAt = new Date(responseBody.updated_at).toISOString();
  expect(responseBody.updated_at).toEqual(parseUpdatedAt);

  //versão do banco
  expect(responseBody.dependencies.database.version).toEqual("16.0");
  expect(responseBody.dependencies.database.max_connections).toEqual(100);

  // Verificar maximo de conexões
  expect(responseBody.dependencies.database.opened_connections).toEqual(1);
  //console.log(responseBody.max_connections);
});
index.js
import database from "infra/database.js";
async function status(request, response) {
  // retorna a data atual em ISO8601
  const updatedAt = new Date().toISOString();

  // Retorna a versão do banco
  const databaseVersionResult = await database.query("SHOW server_version;");
  const databaseVersionValue = databaseVersionResult.rows[0].server_version;

  const databaseMaxConnectionsResult = await database.query(
    "SHOW max_connections",
  );
  const databaseMaxConnectionsValue =
    databaseMaxConnectionsResult.rows[0].max_connections;

  // Retorna atividades no banco postgres
  const databaseOpenedConnectionsResult = await database.query(
    "SELECT * FROM pg_stat_activity WHERE datname='postgres';",
  );
  const databaseOpenedConnectionsValue =
    databaseOpenedConnectionsResult.rows.length; // número de conexões abertas no postgres

  response.status(200).json({
    status: "ok",
    updated_at: updatedAt,
    dependencies: {
      database: {
        version: databaseVersionValue,
        max_connections: parseInt(databaseMaxConnectionsValue),
        opened_connections: databaseOpenedConnectionsValue,
      },
    },
  });
}

export default status;

A resposta em um objeto Json é par chave-valor. response.status(200).json({ chave: "valor" }); Por convenção o nome da variavel deve ser devolvido no formato snake_case, como no exemplo.

Os acentos crase, são templates strings, onde é possivel concatenar variaveis dentro das strings inserindo dentro de um placeholder: ${}

Importando o as configuraçoes do next para jest. O next NÃO carrega para o Jest variáveis de ambiente que estão em um arquivo env.development.

jest.config.js
const dotenv = require("dotenv");
dotenv.config({
  path: ".env.development", // Define o arquivo que estão as variáveis de ambiente
});
// next/jest é um modulo do next que transfere informações para o jest funcionar
const nextJest = require("next/jest");

// devolve uma função para criar um objeto de configuração do jest
const createJestConfig = nextJest({
  dir: ".", // fornece o caminho para o Netx.js para carregar o next.config.js e carregar arquivos .env dentro do ambiente de teste
});
const jestConfig = createJestConfig({
  moduleDirectories: ["node_modules", "<rootDir>"], // permite que o jest acesse os arquivos do projeto
  testTimeout: 60000, //tempo de espera até um teste terminar, por padrão é de 5 segundos.
});
module.exports = jestConfig;

Assuntos Relacionados