Dados de teste no CI/CD: GitHub Actions, pytest, Jest e seed ...
Dados de teste no CI/CD: GitHub Actions, pytest, Jest e seed automatizado
Dados de teste no CI/CD costumam ser commitados em fixtures estáticos (.json, .sql, .csv) e copiados de uma máquina pra outra até alguém esquecer que existem, vazá-los em um screenshot, ou descobrir que metade quebrou depois de uma atualização de schema. Este guia mostra como substituir esse padrão por geração on-demand via API durante o pipeline, com 3 stacks práticas: GitHub Actions + secrets + cache, pytest com fixtures dinâmicos, e Jest com beforeAll para suite Node. No fim, estratégia pra seed pre-deploy em staging sem deixar lixo entre runs.
Por que gerar em vez de commitar
Fixture commitado tem 3 problemas. Primeiro, vira IP morto: ninguém atualiza, ninguém entende, ninguém pode mexer com confiança. Segundo, vaza dado pessoal: CPF, telefone e email reais entram em log de erro, screenshot de Slack, PR de novato. Mesmo dado fictício "parecido com real" causa risco regulatório (LGPD Art. 5º trata como dado pessoal qualquer informação que identifique pessoa física). Terceiro, fica desatualizado: você adiciona campo CNPJ alfanumérico (vigência 01/07/2026), seu fixture de 2024 tem só formato numérico, o teste passa mas a integração no checkout quebra.
Geração on-demand resolve os 3. Você chama a API no setup de cada job, recebe dados frescos com a regra atual, e nada fica em disco depois do run. Custo: latência adicional de ~200ms por chamada (1 chamada por job, geralmente).
Stack 1: GitHub Actions com cache + secrets
Workflow completo que gera 100 customers (pessoa + endereço + cartão correlacionados) antes de rodar a suite de integração. Usa cache pra evitar regenerar dados em jobs que dependem do mesmo dataset, e secrets pra mascarar a API key (mesmo sendo plano gratuito, manda mensagem certa pro time):
# .github/workflows/test.yml
name: Test with BR fixtures
on:
pull_request:
branches: [main]
jobs:
generate-fixtures:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- uses: actions/checkout@v4
- id: cache-key
run: echo "key=fixtures-${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Generate Brazilian customers
env:
FAKEFORGE_KEY: ${{ secrets.FAKEFORGE_API_KEY }}
run: |
mkdir -p .test-fixtures
curl -sS -H "X-API-Key: $FAKEFORGE_KEY" \
"https://fakeforge.com.br/api/generate?preset=customer&quantity=100" \
> .test-fixtures/customers.json
curl -sS -H "X-API-Key: $FAKEFORGE_KEY" \
"https://fakeforge.com.br/api/generate?type=cnpj&quantity=50" \
> .test-fixtures/cnpjs.json
curl -sS -H "X-API-Key: $FAKEFORGE_KEY" \
"https://fakeforge.com.br/api/generate?type=pixKey&quantity=20" \
> .test-fixtures/pix.json
- name: Cache fixtures for downstream jobs
uses: actions/cache/save@v4
with:
path: .test-fixtures
key: ${{ steps.cache-key.outputs.key }}
integration-tests:
needs: generate-fixtures
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore fixtures
uses: actions/cache/restore@v4
with:
path: .test-fixtures
key: ${{ needs.generate-fixtures.outputs.cache-key }}
fail-on-cache-miss: true
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npm run test:integrationTrês coisas que esse workflow faz certo. Job split: o job generate-fixtures roda uma vez e populariza cache; os jobs de teste (você pode ter N: unit, integration, e2e) restauram do cache em vez de chamar a API de novo. Reduz chamadas por PR de N pra 1. Cache key dinâmica: usa github.run_id como chave. Cada PR run tem fixtures novos; nada de cache estagnado entre PRs distintos. Secret pra API key: hábito profissional. Se um dia migrar pro plano Dev (R$29/mês, 10.000 chamadas/dia) ou Team, a key paga vai pelo mesmo caminho, sem refactor.
Stack 2: pytest com fixtures dinâmicos
pytest tem @pytest.fixture com escopo (function, class, module, session). Pra suite de integração, escopo session garante que você gera os dados uma vez por pytest run inteiro, e todos os testes compartilham:
# tests/conftest.py
import os
import pytest
import requests
from typing import TypedDict
API = "https://fakeforge.com.br/api/generate"
KEY = os.getenv("FAKEFORGE_API_KEY") # None em local, valor em CI
class Customer(TypedDict):
name: str
cpf: str
email: str
phone: str
address: dict
@pytest.fixture(scope="session")
def br_customers() -> list[Customer]:
"""100 customers BR correlacionados, gerados uma vez por suite."""
headers = {"X-API-Key": KEY} if KEY else {}
res = requests.get(
API,
params={"preset": "customer", "quantity": 100},
headers=headers,
timeout=10,
)
res.raise_for_status()
return res.json()["data"]
@pytest.fixture(scope="session")
def br_cnpjs() -> list[str]:
res = requests.get(
API,
params={"type": "cnpj", "quantity": 50},
timeout=10,
)
return res.json()["data"]
@pytest.fixture
def random_customer(br_customers):
"""Pega 1 customer aleatório por test function."""
import random
return random.choice(br_customers)Uso no teste:
# tests/test_checkout.py
def test_create_order_with_brazilian_customer(random_customer, client):
response = client.post("/orders", json={
"customer_cpf": random_customer["cpf"],
"customer_email": random_customer["email"],
"shipping_address": random_customer["address"],
"total": 199.90,
})
assert response.status_code == 201
assert response.json()["customer_cpf"] == random_customer["cpf"]
def test_create_business_account(br_cnpjs, client):
cnpj = br_cnpjs[0]
response = client.post("/business-accounts", json={"cnpj": cnpj})
assert response.status_code == 201Pontos de atenção. scope="session" é caro se a chamada falhar (1 falha derruba toda a suite). Em pipeline de produção, vale envelopar com retry exponential backoff (tenacity resolve com 5 linhas). E se você roda pytest local sem internet, use marcador: @pytest.mark.online nos testes que dependem, com pytest -m "not online" pra rodar offline.
Stack 3: Jest com beforeAll no Node
Jest não tem escopo de session nativo, mas beforeAll no nível do test file dá comportamento equivalente. Pra compartilhar entre arquivos, use globalSetup:
// jest.global-setup.ts
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
const TMP = join(process.cwd(), ".jest-fixtures");
export default async () => {
mkdirSync(TMP, { recursive: true });
const customers = await fetch(
"https://fakeforge.com.br/api/generate?preset=customer&quantity=100",
{ headers: { "X-API-Key": process.env.FAKEFORGE_API_KEY || "" } }
).then(r => r.json());
const cards = await fetch(
"https://fakeforge.com.br/api/generate?type=creditCardVisa&quantity=30"
).then(r => r.json());
writeFileSync(join(TMP, "customers.json"), JSON.stringify(customers.data));
writeFileSync(join(TMP, "cards.json"), JSON.stringify(cards.data));
};E no jest.config.ts:
export default {
globalSetup: "<rootDir>/jest.global-setup.ts",
globalTeardown: "<rootDir>/jest.global-teardown.ts",
testEnvironment: "node",
};
// jest.global-teardown.ts
import { rmSync } from "node:fs";
import { join } from "node:path";
export default async () => {
rmSync(join(process.cwd(), ".jest-fixtures"), { recursive: true, force: true });
};Cada test file lê o JSON de .jest-fixtures no beforeAll:
// checkout.test.ts
import { readFileSync } from "node:fs";
import { join } from "node:path";
let customers: Array<{ cpf: string; email: string }>;
beforeAll(() => {
customers = JSON.parse(
readFileSync(join(process.cwd(), ".jest-fixtures/customers.json"), "utf-8")
);
});
test("creates order with BR customer", async () => {
const c = customers[0];
const res = await fetch("/api/orders", {
method: "POST",
body: JSON.stringify({ cpf: c.cpf, email: c.email, total: 99 }),
});
expect(res.status).toBe(201);
});Seed pre-deploy em staging
Diferente do CI (dados de teste consumidos in-memory ou em DB efêmero), o staging é um ambiente persistente que precisa de dados "humanos" pra QA manual, demos pra stakeholders, ou testes exploratórios. Aqui o desafio é diferente: você quer popular uma vez por deploy, e ter como limpar entre versões.
Padrão que funciona bem:
# .github/workflows/deploy-staging.yml
jobs:
deploy:
steps:
- run: ./deploy.sh staging
- name: Reset and seed staging DB
env:
STAGING_DB: ${{ secrets.STAGING_DB_URL }}
FAKEFORGE_KEY: ${{ secrets.FAKEFORGE_API_KEY }}
run: |
# 1. Trunca tabelas com dados de teste anteriores
psql "$STAGING_DB" -c "TRUNCATE customers, orders, addresses RESTART IDENTITY CASCADE;"
# 2. Pede SQL direto da API (formato CREATE TABLE + INSERT)
curl -sS -H "X-API-Key: $FAKEFORGE_KEY" \
-X POST "https://fakeforge.com.br/api/generate" \
-H "Content-Type: application/json" \
-d '{"preset":"customer","quantity":500,"format":"sql"}' \
| psql "$STAGING_DB"O endpoint format=sql retorna CREATE TABLE IF NOT EXISTS ... + INSERT INTO ... pronto pra psql. É a forma mais direta pra reset + seed em uma única passada. Pra MySQL substitua psql por mysql e ajuste o TRUNCATE.
Importante: nunca rode esse mesmo workflow contra produção. Truncate cascade em prod é incidente. Garanta isso com environment: staging no GitHub Actions e branch protection que só permite deploy via PR aprovado.
Quanto custa esse padrão
Free tier do FakeForge tem 50 chamadas/dia. Quanto isso aguenta? Suponha equipe de 5 devs, 4 PRs por dia em média, com 1 chamada de geração por PR (cache rebate o resto): 20 chamadas/dia. Sobram 30 pra deploys em staging e ad-hoc. Funciona por bastante tempo.
Equipe maior ou monorepo com várias suites: plano Dev (R$29/mês, 10.000 chamadas/dia) cobre fácil 50 devs ativos. Team plan (R$79/mês, 100.000 chamadas/dia) é pra org que tá com seed-per-deploy em ambientes paralelos (preview, staging, demo-1, demo-2). Comparação completa no /pricing.
Erros comuns e como evitar
- Chamar a API em todo teste, não no setup. 200 testes × 200ms = 40 segundos de overhead por suite. Use
scope="session"(pytest),globalSetup(Jest), ou job dedicado (Actions). - Commitar a saída em vez de regenerar. Defeat the purpose. Coloca
.test-fixtures/e.jest-fixtures/no.gitignore. - Esquecer de tratar falha de rede. CI sem internet = suite vermelha por motivo errado. Retry + fallback pra fixture local mínimo (10 customers genéricos commitados) cobre caso degenerado.
- Não validar shape do response. A API mudar de campo silenciosamente quebra teste com erro confuso. TypedDict (Python) ou type guard (TypeScript) no
conftest/globalSetupfalha cedo. - Seed em produção por engano. Vide seção anterior. Environment protection no Actions, e variável de ambiente
SEED_ALLOWED=trueque só o staging tem.
Próximos passos
Pra continuar: a documentação completa da API tem os endpoints, presets (customer, employee, ecommerce_order, contact_list) e os formatos de export (JSON, CSV, SQL). Pros stacks específicos de seed: tutorial dedicado de PostgreSQL, MySQL, e Cypress com mock de CEP.
Se você ainda commit fixtures estáticos, este é o momento. Em 2 horas você porta um workflow pra geração on-demand e a próxima atualização de schema brasileiro (CNPJ alfanumérico em julho/2026, qualquer mudança em PIX, novas regras BACEN) chega aos testes sem você precisar mexer nos fixtures.