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 k6No 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 k6No Windows, use o instalador oficial em k6.io/docs/get-started/installation ou instale via Chocolatey:
choco install k6Após a instalação, verifique:
k6 versionAgora 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.jsO 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.
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=10As 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.
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.
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: 14Para 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.jsUma 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);
}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.
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.
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