O número padrão de workers do Playwright é metade dos CPUs lógicos da máquina. Um runner de CI com 2 cores recebe 1 worker e roda os testes sequencialmente por padrão. fullyParallel: true roda cada teste em paralelo independente do arquivo, mas exige que cada teste possua seus dados por completo. Dois testes modificando a mesma linha do banco com um ID fixo vão competir e falhar intermitentemente.
Como o Playwright executa testes por padrão
Antes de mudar qualquer configuração, vale entender o que o Playwright faz por padrão.
Por padrão, o Playwright roda arquivos de teste em paralelo e testes dentro de um único arquivo sequencialmente. Cada processo worker pega um arquivo de teste, roda todos os testes daquele arquivo de cima para baixo e depois pega o próximo arquivo disponível. Múltiplos workers rodam simultaneamente, cada um lidando com um arquivo diferente.
O número padrão de workers é metade dos CPUs lógicos da máquina. Em um laptop típico com 8 cores, você tem 4 workers. Em um runner de CI com 2 cores, você tem 1 worker, o que significa que os testes rodam sequencialmente a menos que você sobrescreva isso.
// playwright.config.ts — comportamento padrão (não precisa de alterações para ter isso)
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// workers padrão é metade dos CPUs lógicos
// testes dentro de um arquivo rodam sequencialmente por padrão
});Esse comportamento padrão é um ponto de partida razoável. Testes no mesmo arquivo frequentemente compartilham estado de setup implicitamente. Mesmo page object, mesmo fluxo de login, mesmas fixtures de dados. A execução sequencial dentro de um arquivo mantém isso seguro. Arquivos separados rodam simultaneamente, o que dá velocidade sem exigir isolamento perfeito entre cada teste.
Configurando workers no playwright.config.ts
A opção workers controla quantos processos paralelos rodam seus testes. Você pode configurar como um número absoluto ou como porcentagem dos CPUs disponíveis.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Número absoluto — sempre usa exatamente 4 workers
workers: 4,
// Ou como porcentagem dos CPUs disponíveis
// workers: '75%',
// Ou valores diferentes por ambiente
// workers: process.env.CI ? 2 : '50%',
});A forma percentual é útil quando você quer que a mesma config funcione em máquinas diferentes. '50%' em uma máquina de 8 cores dá 4 workers; em um runner de CI de 2 cores dá 1. Você está dizendo ao Playwright "use metade da máquina" em vez de fixar um número.
Você também pode sobrescrever workers pela linha de comando sem mexer na config:
# Roda com um número específico de workers
npx playwright test --workers=4
# Força execução sequencial (1 worker)
npx playwright test --workers=1--workers=1 é útil para debugar problemas de isolamento de teste. Se os testes passam com 1 worker mas falham com 4, você tem um problema de estado compartilhado em algum lugar.
--workers=1 primeiro. Se o teste passar consistentemente, você está lidando com uma race condition ou estado compartilhado entre testes, não com um bug no próprio teste.Modo fullyParallel: roda tudo de uma vez
O modo padrão roda arquivos em paralelo mas testes dentro de um arquivo sequencialmente. fullyParallel: true remove essa restrição. Cada teste individual roda em paralelo independente do arquivo em que está.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // Todos os testes rodam em paralelo
workers: 4,
});Isso pode reduzir drasticamente o tempo de execução de suites grandes. Uma suite com 100 testes distribuídos em 10 arquivos (se cada teste demora 2 segundos) cai de 20 segundos para cerca de 5 segundos com 4 workers no modo fullyParallel.
O ponto de atenção: fullyParallel exige que cada teste seja completamente isolado. Sem contexto de navegador compartilhado, sem estado de login compartilhado que seja mutado, sem testes que assumam uma ordem específica de execução. Se seus testes escrevem em um registro de banco compartilhado e dois testes tentam modificar a mesma linha simultaneamente, você vai ter falhas intermitentes difíceis de reproduzir.
Antes de ativar fullyParallel, audite sua suite de testes em busca de:
- Testes que criam dados com IDs fixos (user ID 123 é criado pelo teste A e deletado pelo teste B)
- Testes que dependem de um teste anterior ter rodado primeiro
- Estado de página que não é resetado entre testes
Se seus testes usam test.beforeEach para fazer login do zero e trabalham com dados únicos, fullyParallel é seguro de ativar. Se eles compartilham um contexto de navegador pré-autenticado armazenado em uma variável de nível de módulo, não estão prontos para isso.
test.describe.serial() para testes intencionalmente sequenciais
Às vezes um grupo de testes genuinamente precisa rodar em ordem. Um fluxo de checkout onde o teste 1 adiciona um item ao carrinho, o teste 2 aplica um cupom e o teste 3 completa a compra. Esses testes são inerentemente sequenciais. test.describe.serial() é a ferramenta certa para isso.
import { test, expect } from '@playwright/test';
test.describe.serial('fluxo de checkout', () => {
test('adiciona item ao carrinho', async ({ page }) => {
await page.goto('/products/widget-pro');
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('aplica código de cupom', async ({ page }) => {
await page.goto('/cart');
await page.getByPlaceholder('Coupon code').fill('SAVE10');
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByTestId('discount-amount')).toBeVisible();
});
test('completa a compra', async ({ page }) => {
await page.goto('/checkout');
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/26');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});
});Com test.describe.serial(), o Playwright roda esses três testes em ordem e para se algum falhar. Não faz sentido rodar "completa a compra" se "adiciona item ao carrinho" falhou.
Use serial com moderação. Cada bloco serial é uma seção da sua suite que não pode ser paralelizada. Se você se encontra adicionando serial na maioria dos blocos describe, a correção real é tornar seus testes independentes entre si. Gere dados de teste únicos, use contextos de navegador isolados e limpe após cada teste.
Isolamento de testes: o pré-requisito para execução paralela
A execução paralela amplifica problemas de isolamento. Um teste que funciona bem sozinho vai falhar de forma imprevisível quando roda ao mesmo tempo que outro teste que toca os mesmos dados ou estado.
O princípio central: cada teste deve possuir seus dados e não depender de nada deixado por outro teste.
import { test, expect } from '@playwright/test';
// RUIM: estado compartilhado entre testes
let userId: number;
test('cria um usuário', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@example.com' }
});
userId = (await response.json()).id; // variável compartilhada — race condition esperando para acontecer
});
test('atualiza o usuário', async ({ request }) => {
// Se o teste anterior ainda não rodou (ou rodou em outro worker), userId é undefined
await request.put(`/api/users/${userId}`, {
data: { name: 'Alice Updated' }
});
});import { test, expect } from '@playwright/test';
// BOM: cada teste cria e possui seus próprios dados
test('atualiza um usuário', async ({ request }) => {
// Cria o usuário dentro deste teste
const createResponse = await request.post('/api/users', {
data: {
name: 'Alice',
email: `alice-${Date.now()}@example.com` // email único previne conflitos
}
});
const { id } = await createResponse.json();
// Agora atualiza — este usuário é nosso
const updateResponse = await request.put(`/api/users/${id}`, {
data: { name: 'Alice Updated' }
});
expect(updateResponse.status()).toBe(200);
});Para testes de UI, o fixture page do Playwright dá a cada teste seu próprio contexto de navegador por padrão. Essa parte é tratada automaticamente. Os problemas de isolamento geralmente vêm de dados de teste em um banco compartilhado, não do estado do navegador.
test.beforeAll para criar dados compartilhados e test.afterAll para limpá-los parece eficiente, mas cria dependências ocultas entre testes. Se um teste modifica os dados compartilhados, os testes seguintes quebram. Prefira test.beforeEach com dados por teste, mesmo que seja mais lento.Sharding: dividindo sua suite entre máquinas de CI
Workers paralelizam testes dentro de uma máquina. Sharding divide a suite de testes entre várias máquinas. Esses dois mecanismos são independentes e complementares. Você pode usar os dois juntos.
A flag --shard recebe um argumento atual/total:
# Roda o shard 1 de 3 (primeiro terço dos testes)
npx playwright test --shard=1/3
# Roda o shard 2 de 3
npx playwright test --shard=2/3
# Roda o shard 3 de 3
npx playwright test --shard=3/3O Playwright distribui arquivos de teste igualmente entre os shards. Com 30 arquivos de teste e 3 shards, cada shard recebe 10 arquivos. A distribuição é determinística. Você vai ter os mesmos arquivos no mesmo shard em cada execução.
Você pode combinar sharding com workers. Cada shard roda com múltiplos workers, então você tem paralelismo tanto dentro do shard quanto entre os shards:
# Cada shard usa 4 workers internamente
npx playwright test --shard=1/3 --workers=4Sharding é principalmente valioso no CI, onde você pode provisionar múltiplas máquinas para uma única execução de pipeline.
GitHub Actions matrix para sharding paralelo
O GitHub Actions suporta builds de matrix, rodando um job múltiplas vezes com inputs diferentes. Combinado com sharding do Playwright, é assim que você divide uma suite de testes lenta entre máquinas paralelas.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run tests (shard ${{ matrix.shard }}/4)
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ vars.BASE_URL }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
- name: Upload shard report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 7fail-fast: false é importante aqui. Por padrão, se um job da matrix falha, o GitHub cancela os jobs restantes. Com fail-fast: false, todos os shards rodam até o fim mesmo que um falhe. Você tem o quadro completo do que passou e falhou em toda a suite.
O argumento install chromium na etapa de instalação do navegador economiza tempo. Se você roda testes cross-browser, mude isso para --with-deps sem especificar um navegador para instalar os três.
Para mesclar os relatórios dos shards em um único relatório, adicione um job de merge após o matrix:
merge-reports:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Download all shard reports
uses: actions/download-artifact@v4
with:
pattern: playwright-report-shard-*
path: all-reports/
merge-multiple: false
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-reports/*/
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: playwright-report-merged
path: playwright-report/
retention-days: 14Isso dá um único relatório HTML baixável cobrindo todos os shards, com todos os resultados de teste em um lugar.
Medindo speedup e encontrando o número certo de workers
Mais workers nem sempre significa testes mais rápidos. Adicionar workers aumenta a disputa por recursos: mais CPU, mais memória, mais processos de navegador competindo pela mesma máquina. Em algum ponto, adicionar um worker desacelera as coisas porque a máquina está sobrecarregada.
A regra geral: workers = número de CPUs lógicos funciona bem para workloads que consomem muito CPU. Para testes de navegador, que ficam a maior parte do tempo aguardando rede e renderização, muitas vezes você pode ir mais alto. Workers = 2x CPUs é um experimento razoável.
Como medir:
# Baseline: 1 worker (sequencial)
npx playwright test --workers=1 2>&1 | grep "passed\|failed\|Duration"
# Tenta 2 workers
npx playwright test --workers=2 2>&1 | grep "passed\|failed\|Duration"
# Tenta 4 workers
npx playwright test --workers=4 2>&1 | grep "passed\|failed\|Duration"
# Tenta 8 workers
npx playwright test --workers=8 2>&1 | grep "passed\|failed\|Duration"Plote os resultados. Você está procurando o ponto de inflexão onde adicionar workers para de reduzir o tempo. Esse é o número ótimo para aquela máquina.
Para CI especificamente, verifique os recursos que seu runner oferece. Os runners ubuntu-latest do GitHub Actions têm 4 vCPUs e 16 GB de RAM. Com testes de navegador Playwright, 4 workers é um bom ponto de partida. Você pode ficar 5 a 10% mais rápido com mais, mas começa a ter pressão de memória com 8 ou mais workers nesse runner.
Uma fórmula prática para calcular o benefício do sharding:
Tempo com N shards ≈ (tempo total de teste em 1 máquina) / N + overhead fixo por shard
Overhead fixo = checkout + npm ci + instalação de browser ≈ 60-90 segundosSe sua suite demora 10 minutos com 1 máquina, 4 shards trazem isso para cerca de 2,5 minutos + 90 segundos de overhead = ~4 minutos. Vale a pena. Se sua suite demora 3 minutos, 4 shards trazem isso para 45 segundos + 90 segundos = 2,5 minutos. Não compensa a complexidade adicional.
O threshold do sharding: comece a considerar sharding quando sua suite consistentemente demora mais de 5 minutos em uma única máquina de CI.
// playwright.config.ts — config paralela pronta para produção
import { defineConfig } from '@playwright/test';
const isCI = !!process.env.CI;
export default defineConfig({
testDir: './tests',
// Paralelismo total — exige testes isolados
fullyParallel: true,
// Ajusta workers para o ambiente
workers: isCI ? 4 : '50%',
// Retries só no CI — não mascara falhas localmente
retries: isCI ? 1 : 0,
// Timeout por teste
timeout: 30_000,
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
},
});FAQ
Meus testes passam localmente mas falham quando rodam em paralelo. Por onde começo?Rode com --workers=1 e confirme que os testes passam. Depois tente --workers=2. Se falhar, você tem um problema de estado compartilhado entre exatamente dois testes que agora rodam simultaneamente. Verifique variáveis de nível de módulo, linhas de banco com IDs fixos, ou qualquer estado que persiste entre testes. A correção é quase sempre mover o setup para beforeEach e usar identificadores únicos para os dados de teste.
O Playwright ordena os arquivos de teste alfabeticamente e os distribui em round-robin entre os shards. Você não controla a atribuição diretamente. Se um shard consistentemente demora mais que os outros (aquele shard tem todos os testes lentos), divida arquivos de teste grandes em menores para que a distribuição fique mais uniforme.
Posso rodar tags específicas ou padrões grep por shard em vez de usar--shard?
Sim, e alguns times preferem isso pela previsibilidade: --grep @checkout em uma máquina e --grep @catalog em outra. A desvantagem é manutenção manual: você precisa atualizar os padrões grep conforme adiciona testes. --shard é automático e sem manutenção.
fullyParallel: true afeta a ordem em que os resultados aparecem no relatório?
Sim. Com fullyParallel, os resultados aparecem conforme os testes completam, não em ordem de arquivo. O relatório HTML ainda agrupa por arquivo e teste, então a legibilidade não é afetada. O output do terminal apenas parece mais intercalado.
workers na config e --shard na linha de comando?
workers controla o paralelismo dentro de um processo em uma máquina. --shard divide a suite entre múltiplas invocações, tipicamente em máquinas diferentes. Eles operam em níveis diferentes e funcionam juntos. Cada shard pode ter múltiplos workers.
→ Veja também: Depurando Testes Instáveis: Um Guia Prático | CI/CD para QA: GitHub Actions, Jenkins e GitLab Comparados | GitHub Actions para Testes Playwright: A Configuração Completa (2026) | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado | Arquivo de Configuração do Playwright Explicado: Todas as Opções