Diferente das assertions do Playwright, um check com falha no k6 não para o teste: todos os usuários virtuais continuam rodando. O k6 contabiliza as taxas de aprovação/reprovação em milhares de requisições para mostrar o sinal de saúde no resumo. O que para a execução é um threshold com falha. Se o tempo de resposta no p95 ultrapassar o limite configurado, o k6 sai com código 99 e o step de CI falha exatamente como um teste que quebrou.

O que é o k6 e por que QA engineers devem conhecê-lo

O k6 é uma ferramenta open source de testes de performance criada pelo Grafana Labs. Você escreve scripts em JavaScript ou TypeScript, roda pelo CLI e recebe uma saída detalhada sobre como seu sistema se comportou sob carga. O repositório no GitHub tem mais de 24.000 stars e é usado por organizações de todos os tamanhos.

O que torna o k6 o ponto de entrada certo para QA engineers é o modelo de scripting. Se você já escreve testes com Playwright ou Jest, a sintaxe é familiar: você importa módulos, escreve funções, faz requisições HTTP e adiciona assertions. Sem GUI pesada, sem formato proprietário de gravação, sem arquivo de configuração XML. Apenas um script.

O k6 também se integra bem com o GitHub Actions. Você instala o binário, roda o script e, se o teste falha (mais sobre o que "falhar" significa adiante), o step de CI sai com código não-zero. É o mesmo contrato que todas as outras ferramentas de teste usam.

A ferramenta não substitui o Playwright. O Playwright verifica comportamento; o k6 mede performance sob carga. Eles respondem perguntas diferentes e os dois pertencem a uma estratégia de QA madura.

Instalando o k6 e rodando seu primeiro teste

O k6 é distribuído como um binário standalone. No macOS:

brew install k6

No Linux (Debian/Ubuntu):

sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

No Windows, use o instalador oficial em k6.io/docs/get-started/installation ou instale via Chocolatey:

choco install k6

Após a instalação, verifique:

k6 version

Agora crie um arquivo chamado load-test.js e adicione este script mínimo:

import http from 'k6/http';

export default function () {
  http.get('https://test-api.k6.io/public/crocodiles/');
}

Execute:

k6 run load-test.js

O k6 usa test-api.k6.io como sandbox público. Você pode rodar contra ele sem configurar nada. Vai aparecer no terminal a contagem de requisições, os tempos de resposta e uma tabela de resumo. Esse é seu primeiro load test rodando.

Anatomia de um script k6

Um script k6 real tem três partes. O export options controla como o teste roda, a função default contém a lógica do teste, e o setup prepara o que suas requisições precisam. Veja a estrutura:

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  vus: 10,          // usuários virtuais rodando simultaneamente
  duration: '30s',  // quanto tempo o teste roda
};

export default function () {
  http.get('https://sua-api.exemplo.com/api/products');
  sleep(1); // tempo de espera entre requisições
}

vus significa virtual users (usuários virtuais). Cada VU roda a função default em loop pela duração total, independentemente dos outros. Configurar vus: 10 e duration: '30s' significa 10 usuários simulados acessando seu endpoint continuamente por 30 segundos.

A chamada sleep(1) não é opcional: ela modela o tempo de pensamento que um usuário real levaria entre ações. Sem ela, cada VU dispara requisições tão rápido quanto o servidor consegue responder, o que é irreal e produz números de throughput enganosamente altos. Um segundo de sleep é um padrão razoável; ajuste para corresponder ao comportamento real dos seus usuários.

Os usuários virtuais do k6 não são threads ou processos — são goroutines leves. Uma única instância do k6 pode rodar milhares de VUs realisticamente sem consumir uma quantidade proporcional de memória ou CPU. Você não precisa de um cluster para rodar um load test útil.

Para requisições POST com body JSON, necessárias para a maioria dos testes de API:

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  vus: 20,
  duration: '1m',
};

export default function () {
  const url = 'https://sua-api.exemplo.com/api/login';
  const payload = JSON.stringify({
    username: 'testuser@exemplo.com',
    password: 'senhateste',
  });
  const params = {
    headers: { 'Content-Type': 'application/json' },
  };

  http.post(url, payload, params);
  sleep(1);
}

Lendo a saída do k6

Após uma execução, o k6 imprime uma tabela de resumo no terminal. Aprender a lê-la rapidamente é a habilidade que transforma números brutos em informação acionável.

Uma saída típica fica assim:

✓ checks.........................: 100.00% ✓ 1800 ✗ 0
  data_received..................: 4.3 MB  143 kB/s
  data_sent......................: 523 kB  17 kB/s
  http_req_blocked...............: avg=1.2ms    min=1µs    med=4µs    max=312ms  p(90)=7µs    p(95)=11µs
  http_req_connecting............: avg=743µs    min=0s     med=0s     max=181ms  p(90)=0s     p(95)=0s
  http_req_duration..............: avg=312ms    min=98ms   med=280ms  max=2.1s   p(90)=520ms  p(95)=640ms
    { expected_response:true }...: avg=312ms    min=98ms   med=280ms  max=2.1s   p(90)=520ms  p(95)=640ms
  http_req_failed................: 0.00%   ✓ 0 ✗ 1800
  http_req_receiving.............: avg=2.1ms    min=45µs   med=1.1ms  max=74ms   p(90)=4.2ms  p(95)=5.8ms
  http_req_sending...............: avg=178µs    min=27µs   med=136µs  max=3.2ms  p(90)=312µs  p(95)=389µs
  http_req_tls_handshaking.......: avg=412µs    min=0s     med=0s     max=168ms  p(90)=0s     p(95)=0s
  http_req_waiting...............: avg=310ms    min=97ms   med=278ms  max=2.1s   p(90)=518ms  p(95)=637ms
  http_reqs......................: 1800    60.0/s
  iteration_duration.............: avg=1.31s    min=1.1s   med=1.28s  max=3.1s   p(90)=1.52s  p(95)=1.64s
  iterations.....................: 1800    60.0/s
  vus............................: 10      min=10     max=10
  vus_max........................: 10      min=10     max=10

As três métricas que você olha primeiro:

http_req_duration é o tempo total de ida e volta de cada requisição. A coluna p(95) mostra o percentil 95: 95% das suas requisições completaram dentro desse tempo. Esse é o número que importa para SLAs, não a média. Médias escondem outliers; p95 não. http_reqs mostra o total de requisições feitas e a taxa por segundo. Diz quanto de carga você realmente gerou. http_req_failed mostra o percentual de requisições que receberam resposta de erro (4xx ou 5xx). Zero porcento é o esperado; qualquer valor acima disso exige investigação.

Adicionando checks

Correção funcional sob carga importa tanto quanto velocidade. O k6 tem um mecanismo nativo de assertions chamado checks, que funciona de forma similar às assertions de outros frameworks de teste:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 10,
  duration: '30s',
};

export default function () {
  const res = http.get('https://sua-api.exemplo.com/api/products');

  check(res, {
    'status é 200': (r) => r.status === 200,
    'tempo de resposta < 500ms': (r) => r.timings.duration < 500,
    'body contém produtos': (r) => r.body.includes('"id"'),
  });

  sleep(1);
}

A diferença importante em relação às assertions tradicionais: um check com falha não para o teste. Todos os VUs continuam rodando. O k6 contabiliza quantos checks passaram e falharam em todas as iterações e mostra o percentual no resumo. Isso é intencional. Você quer saber qual porcentagem das requisições sob carga produziu uma resposta correta, não apenas se uma requisição falhou.

Use checks para tudo que você normalmente assertaria em um teste funcional: código de status, tempo de resposta, presença de campos esperados no body da resposta. A taxa de aprovação dos checks no resumo dá um sinal de saúde combinado em milhares de requisições.

Para um endpoint de login que retorna um token:

import http from 'k6/http';
import { check, sleep } from 'k6';

export default function () {
  const payload = JSON.stringify({
    username: 'user@exemplo.com',
    password: 'senha123',
  });

  const res = http.post('https://sua-api.exemplo.com/auth/login', payload, {
    headers: { 'Content-Type': 'application/json' },
  });

  check(res, {
    'login com sucesso': (r) => r.status === 200,
    'token presente': (r) => JSON.parse(r.body).token !== undefined,
    'login rápido o suficiente': (r) => r.timings.duration < 1000,
  });

  sleep(1);
}

Stages: ramp up, sustentação e ramp down

Uma carga constante e flat raramente é o que você quer. O tráfego real aumenta conforme os usuários chegam, se mantém no pico e depois cai. O k6 modela isso com stages, que permitem definir como a contagem de VUs muda ao longo do tempo:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // subir para 50 VUs em 2 minutos
    { duration: '5m', target: 50 },   // manter 50 VUs por 5 minutos
    { duration: '2m', target: 100 },  // subir para 100 VUs em 2 minutos
    { duration: '5m', target: 100 },  // manter 100 VUs por 5 minutos
    { duration: '2m', target: 0 },    // descer para 0
  ],
};

export default function () {
  const res = http.get('https://sua-api.exemplo.com/api/products');

  check(res, {
    'status é 200': (r) => r.status === 200,
    'tempo de resposta ok': (r) => r.timings.duration < 1000,
  });

  sleep(1);
}

Esse padrão (subir, sustentar, subir mais, sustentar, descer) é chamado de perfil de stress test. Revela em qual contagem de usuários seu sistema começa a degradar. Você vai ver o tempo de resposta no p95 subir gradualmente nos resultados conforme os VUs aumentam. O ponto de inflexão onde ele dispara bruscamente é o ponto de ruptura do sistema.

Para a maioria dos testes de API, você vai rodar um teste mais simples em duas fases: subir e manter.

export const options = {
  stages: [
    { duration: '1m', target: 50 },  // aquecimento
    { duration: '5m', target: 50 },  // carga sustentada
    { duration: '30s', target: 0 },  // ramp down
  ],
};

A fase de ramp down importa. Cortar a carga abruptamente pode mascarar problemas de pool de conexões e dá métricas mais limpas no final do teste.

Thresholds: fazendo o teste passar ou falhar

Os checks dizem o que aconteceu. Os thresholds determinam se a execução do teste teve sucesso ou falhou. Um threshold é uma condição de aprovação/reprovação em qualquer métrica. Se a condição é violada, o k6 sai com código não-zero. Esse é o gancho que torna os load tests úteis no CI.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<2000'],   // percentil 95 deve ser menor que 2 segundos
    http_req_failed: ['rate<0.01'],      // menos de 1% das requisições podem falhar
    checks: ['rate>0.99'],               // mais de 99% dos checks devem passar
  },
};

export default function () {
  const res = http.get('https://sua-api.exemplo.com/api/products');

  check(res, {
    'status é 200': (r) => r.status === 200,
    'tempo de resposta < 2s': (r) => r.timings.duration < 2000,
  });

  sleep(1);
}

Quando o k6 roda esse script e o tempo de resposta no p95 ultrapassa 2000ms, a execução sai com código 99. Seu step de CI falha. Esse é o comportamento correto. Um build que faz sua API ficar 40% mais lenta deve falhar.

Você pode definir thresholds por endpoint usando tags:

export default function () {
  const loginRes = http.post(
    'https://sua-api.exemplo.com/auth/login',
    JSON.stringify({ username: 'user@exemplo.com', password: 'senha' }),
    { headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
  );

  const productsRes = http.get(
    'https://sua-api.exemplo.com/api/products',
    { tags: { name: 'products' } }
  );
}

export const options = {
  thresholds: {
    'http_req_duration{name:login}': ['p(95)<1000'],
    'http_req_duration{name:products}': ['p(95)<500'],
  },
};

O login é esperado ser mais lento do que uma lista de produtos em cache, então você dá a cada endpoint seu próprio SLA.

Os thresholds são avaliados continuamente durante o teste, não apenas ao final. Se o p95 ultrapassar o limite no início da execução e se recuperar depois, o threshold ainda é marcado como reprovado. Isso é intencional. Um sistema que degrada e se recupera pode estar escondendo um problema de capacidade que precisa ser investigado.

Rodando o k6 no GitHub Actions

O k6 roda bem no GitHub Actions. O time do Grafana publica uma action oficial que instala o binário e gerencia o versionamento:

# .github/workflows/load-test.yml
name: Load Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  load-test:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout do código
        uses: actions/checkout@v4

      - name: Instalar k6
        uses: grafana/setup-k6-action@v1
        with:
          k6-version: '0.55.0'

      - name: Rodar load test
        run: k6 run load-test.js
        env:
          BASE_URL: ${{ vars.BASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}

      - name: Upload dos resultados do k6
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: k6-results
          path: k6-results.json
          retention-days: 14

Para usar variáveis de ambiente dentro do script k6, acesse-as via __ENV:

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export const options = {
  stages: [
    { duration: '1m', target: 20 },
    { duration: '3m', target: 20 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<2000'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const res = http.get(`${BASE_URL}/api/products`);

  check(res, {
    'status é 200': (r) => r.status === 200,
    'tempo de resposta aceitável': (r) => r.timings.duration < 2000,
  });

  sleep(1);
}

Para gerar um arquivo JSON de resultados para o upload de artefato, passe a flag --out:

k6 run --out json=k6-results.json load-test.js

Uma recomendação prática: não rode load tests em todo pull request contra seu ambiente de produção ou staging compartilhado. O job de load test deve rodar em merges para a main, ou ser disparado via workflow_dispatch manualmente. Rodar load tests pesados em cada branch de PR causa interferência entre execuções simultâneas. Um teste no nível de smoke (5 VUs, 30 segundos) em PRs é aceitável; um load test completo não é.

Quais endpoints testar

Escolher o que testar com o k6 é tão importante quanto o próprio teste. Nem todo endpoint precisa de load test. Foque nos que a performance importa.

O endpoint de login é o primeiro teste a escrever. Toda sessão de usuário começa aqui. Um login lento multiplicado por 500 usuários simultâneos destrói a experiência antes de qualquer um chegar às funcionalidades reais. Teste o fluxo completo de login: POST com credenciais, receber token, fazer uma requisição autenticada.

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export const options = {
  stages: [
    { duration: '1m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<1500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  // Passo 1: Login
  const loginRes = http.post(
    `${BASE_URL}/auth/login`,
    JSON.stringify({ username: 'loadtest@exemplo.com', password: 'senhaloadtest' }),
    { headers: { 'Content-Type': 'application/json' } }
  );

  check(loginRes, {
    'login status 200': (r) => r.status === 200,
    'token retornado': (r) => JSON.parse(r.body).token !== undefined,
  });

  const token = JSON.parse(loginRes.body).token;

  // Passo 2: Requisição autenticada
  const profileRes = http.get(`${BASE_URL}/api/me`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  check(profileRes, {
    'perfil status 200': (r) => r.status === 200,
  });

  sleep(2);
}

Endpoints críticos de leitura são a segunda prioridade. Listagem de produtos, resultados de busca, dados do dashboard: qualquer coisa que os usuários acessam repetidamente e que acessa o banco de dados. São os endpoints com maior probabilidade de degradação em escala porque agregam dados de múltiplas linhas ou tabelas. O fluxo de checkout ou envio de pedido é o terceiro. É o endpoint onde performance lenta tem impacto direto em receita. Teste com um VU count menor do que seus endpoints de leitura (a concorrência real de checkout é menor do que a de navegação). Exija um threshold de tempo de resposta mais rigoroso.

Um ponto de partida razoável para a maioria das aplicações são três scripts de load test. Um para autenticação, um para seus três principais endpoints de leitura em um único script, e um para sua operação de escrita crítica para conversão. Cobertura suficiente para capturar as regressões de performance que importam sem construir uma suite completa antes de ter uma baseline.

FAQ

Preciso de um ambiente de teste separado para rodar o k6?

Você precisa de um ambiente que aguente a carga sem afetar usuários reais ou corromper dados de produção. Um ambiente dedicado para load test é o ideal. Se você só tem staging, rode os load tests fora do horário comercial e garanta que seus dados de teste estejam isolados. Nunca aponte o k6 para produção com VU counts altos.

Quantos VUs devo usar?

Comece com um número que represente o tráfego de pico realista, não o máximo teórico. Se sua aplicação tem 50 usuários simultâneos num dia movimentado, teste com 50 a 100. Descobrir o que acontece com 1.000 VUs importa menos do que saber que o sistema aguenta a carga de pico normal confortavelmente.

Meu teste k6 passa mas a aplicação parece lenta. Por quê?

O k6 mede o tempo de resposta HTTP, que inclui o processamento do servidor, mas exclui a renderização no cliente. Um endpoint de API que responde em 200ms ainda pode parecer lento se o frontend faz 20 chamadas sequenciais para popular uma página. Use o k6 para performance em nível de API; use ferramentas de profiling do browser para performance percebida pelo usuário.

O k6 consegue testar endpoints WebSocket ou gRPC?

Sim. O k6 tem suporte nativo para WebSockets via o módulo k6/ws e para gRPC via o módulo k6/net/grpc. O modelo de scripting é o mesmo; apenas a API específica do protocolo muda.

Como compartilho os resultados do k6 com meu time?

O arquivo JSON de saída do --out json pode ser importado no Grafana para visualização. Se seu time usa o Grafana Cloud, o k6 tem integração nativa que transmite resultados em tempo real e os armazena para comparação entre execuções. Para times sem Grafana, a tabela de resumo do terminal copiada em um comentário de PR ou mensagem no Slack é suficiente para comunicar aprovação/reprovação e os percentis principais.

Qual a diferença entre load test e stress test?

Load testing verifica se o sistema atende aos requisitos de performance no nível de tráfego esperado. Stress testing empurra além do esperado para encontrar o ponto de ruptura. A configuração de stages lida com os dois: um load test mantém em uma contagem alvo de VUs; um stress test continua subindo até os checks falharem.

→ Veja também: Fundamentos de Testes de Performance: Load, Stress e Spike Testing | Testes de API 101: Tudo que Todo Engenheiro QA Precisa Saber em 2026 | CI/CD para QA: GitHub Actions, Jenkins e GitLab Comparados