Atualizado em 19/06/2026
Quem desenvolve para WordPress conhece a sensação: você ajusta um filtro, atualiza um plugin ou sobe uma nova versão do PHP e algo silenciosamente quebra três telas adiante. Testes automatizados existem para transformar esse medo em uma rede de segurança. Em vez de clicar manualmente por toda a interface depois de cada mudança, você escreve uma vez o comportamento esperado e deixa o PHPUnit verificar — em segundos, a cada commit. Este guia cobre a suíte de testes oficial do WordPress, do scaffold à integração contínua, com as armadilhas que costumam custar uma tarde inteira a quem está começando.
Por que testar (e o que se ganha)
O retorno dos testes aparece em três momentos concretos do dia a dia:
- Detectar regressões. Um teste que passava e começou a falhar aponta exatamente o que sua última mudança quebrou — antes do cliente perceber.
- Refatorar com segurança. Com a suíte verde, você pode reescrever uma função interna sabendo que, se o resultado externo mudar, o teste avisa. Sem testes, todo refactor é uma aposta.
- Documentar o comportamento. Um bom teste descreve, em código executável, o que o seu plugin deveria fazer. É documentação que nunca fica desatualizada, porque o CI a verifica.
Isso é especialmente valioso ao manter um plugin próprio que precisa sobreviver a anos de atualizações de WordPress, PHP e WooCommerce.
A suíte de testes do WordPress (wp-phpunit)
O WordPress não testa o seu código com um PHPUnit "pelado": ele fornece uma biblioteca de testes (a WordPress PHPUnit test library, distribuída também como o pacote Composer wp-phpunit/wp-phpunit) que sobe uma instalação real do WordPress em memória antes de cada execução. É essa biblioteca que define a classe-base WP_UnitTestCase, as fixtures, a factory de dados e o rollback do banco entre testes.
Na prática, isso significa que dentro dos seus testes você tem acesso a get_option(), WP_Query, do_action() e qualquer função do core — exatamente como em uma requisição normal. Por isso a suíte é, em essência, um arcabouço de testes de integração: o objeto sob teste e o WordPress estão juntos, não isolados.
Pré-requisitos. Você vai precisar de PHP na linha de comando, do WP-CLI, de um servidor MySQL/MariaDB acessível e (recomendado) do Composer e do utilitário subversion ou git, que o script de instalação usa para baixar a biblioteca de testes do core.
Gerando o scaffold com WP-CLI
O caminho mais rápido para começar é deixar o WP-CLI montar o esqueleto. Dentro de uma instalação WordPress, com o seu plugin já presente em wp-content/plugins/meu-plugin, rode:
<?php /* (terminal) */ ?>
# Gera os arquivos de teste para o plugin "meu-plugin"
wp scaffold plugin-tests meu-plugin
# O comando cria, dentro da pasta do plugin:
# bin/install-wp-tests.sh -> instala WordPress + a lib de testes
# phpunit.xml.dist -> configuração do PHPUnit
# tests/bootstrap.php -> carrega a lib e o seu plugin
# tests/test-sample.php -> um teste de exemplo
# .phpcs.xml.dist -> padrão de code style (opcional)
O wp scaffold plugin-tests não inventa nada: ele apenas escreve os arquivos no formato que o core e a comunidade adotam, poupando você de copiá-los de um projeto antigo. Cada um deles tem um papel específico, resumido na tabela mais adiante.
Instalando o ambiente de testes
Com o scaffold no lugar, o próximo passo é instalar o WordPress de teste e a biblioteca. O install-wp-tests.sh recebe, nesta ordem: nome do banco de testes, usuário, senha, host e versão do WordPress.
<?php /* (terminal) */ ?>
# Sintaxe:
# bin/install-wp-tests.sh <db_name> <db_user> <db_pass> [db_host] [wp_version]
# Exemplo: banco DEDICADO chamado "wordpress_test"
bash bin/install-wp-tests.sh wordpress_test root "senha" 127.0.0.1 latest
# Instala as dependências do Composer (PHPUnit, etc.) e roda a suíte
composer install
vendor/bin/phpunit
Armadilha que destrói dados. O banco informado em <db_name> é derrubado e recriado (DROP + CREATE) toda vez que o script roda. Nunca, em hipótese alguma, aponte para o banco de produção ou para um banco de desenvolvimento que contenha algo que você não pode perder. Use um nome inconfundível como wordpress_test e, de preferência, um MySQL local ou em container, isolado do resto.
O script grava os arquivos da biblioteca de testes em um diretório temporário (por padrão sob /tmp/wordpress-tests-lib) e o WordPress de teste em /tmp/wordpress. O tests/bootstrap.php aponta para esse local através da variável de ambiente WP_TESTS_DIR; se você instalar a lib em outro lugar, exporte essa variável antes de rodar o PHPUnit.
Escrevendo o primeiro teste com WP_UnitTestCase
A classe WP_UnitTestCase estende a TestCase do PHPUnit e adiciona tudo que é específico do WordPress: as fixtures, o acesso à factory e o rollback automático do banco entre os testes — feito via transações de banco, o que torna a suíte rápida mesmo criando muitos registros.
<?php
class Test_Minha_Funcao extends WP_UnitTestCase {
public function set_up() {
parent::set_up();
// Preparação comum a vários testes pode vir aqui.
}
public function test_post_recem_criado_fica_publicado() {
// A factory cria um post REAL no banco de testes e devolve o ID.
$post_id = $this->factory()->post->create( array(
'post_title' => 'Olá, testes',
'post_status' => 'publish',
) );
$post = get_post( $post_id );
$this->assertInstanceOf( 'WP_Post', $post );
$this->assertEquals( 'publish', $post->post_status );
$this->assertEquals( 'Olá, testes', $post->post_title );
}
public function test_usuario_editor_existe() {
$user_id = $this->factory()->user->create( array( 'role' => 'editor' ) );
$user = get_userdata( $user_id );
$this->assertTrue( in_array( 'editor', $user->roles, true ) );
}
public function tear_down() {
parent::tear_down();
}
}
Note set_up()/tear_down() (com underscore): as versões recentes da suíte do WordPress usam esses nomes para conviver com diferentes versões do PHPUnit, em vez do setUp()/tearDown() clássico — sempre chame o parent:: correspondente para não quebrar o ciclo de vida das fixtures.
Testando hooks e filtros
Como quase toda customização de WordPress passa por actions e filters, saber testá-los é essencial. O padrão é: registrar o filtro (ou garantir que o seu plugin o registrou), disparar o ponto que o aplica e verificar o resultado.
<?php
class Test_Filtro_Titulo extends WP_UnitTestCase {
public function test_filtro_adiciona_sufixo_ao_titulo() {
// Registramos um filtro no próprio teste.
$callback = function ( $title ) {
return $title . ' — Loja';
};
add_filter( 'the_title', $callback );
$resultado = apply_filters( 'the_title', 'Produto X' );
$this->assertEquals( 'Produto X — Loja', $resultado );
// Boa prática: remover o que você adicionou.
remove_filter( 'the_title', $callback );
}
public function test_meu_plugin_filtra_excerpt_length() {
// Aqui assumimos que o plugin já fez add_filter( 'excerpt_length', ... )
$tamanho = apply_filters( 'excerpt_length', 55 );
// Esperamos que o plugin tenha reduzido para 20.
$this->assertSame( 20, $tamanho );
}
}
Para erros, a suíte do WordPress oferece a asserção assertWPError(), que verifica se um valor é uma instância de WP_Error — útil ao testar funções do core que retornam erro em vez de lançar exceção, como wp_insert_post() em condições inválidas.
| Conceito / ferramenta | Para que serve |
|---|---|
WP_UnitTestCase | Classe-base que estende o PHPUnit com fixtures do WordPress e rollback do banco entre testes. |
$this->factory() | Cria dados de teste reais (posts, usuários, termos, comentários) e devolve seus IDs. |
tests/bootstrap.php | Carrega a biblioteca de testes e o seu plugin antes de qualquer teste rodar. |
bin/install-wp-tests.sh | Baixa o WordPress de teste e a lib, e cria o banco de testes (que é recriado a cada execução). |
phpunit.xml.dist | Configura o PHPUnit: onde estão os testes, o bootstrap e quais diretórios cobrir. |
assertWPError() | Asserção específica para validar retornos do tipo WP_Error. |
Configuração: phpunit.xml.dist
O arquivo gerado pelo scaffold já vem pronto, mas vale entender o essencial. Ele indica o bootstrap, registra a suíte e (opcionalmente) define cobertura:
<?php /* phpunit.xml.dist */ ?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true">
<testsuites>
<testsuite name="meu-plugin">
<directory suffix="test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
O atributo backupGlobals="false" é deliberado: fazer backup de todas as globais a cada teste seria lento e, no contexto do WordPress, contraproducente. Em compensação, a responsabilidade de não vazar estado global passa a ser sua (veja as armadilhas).
Integração contínua com GitHub Actions
O ganho real vem quando a suíte roda sozinha a cada push e pull request. O workflow abaixo sobe um MySQL, instala o ambiente de testes e dispara o PHPUnit — e usa uma matrix para cobrir mais de uma versão de PHP:
<?php /* .github/workflows/tests.yml */ ?>
name: Testes PHPUnit
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4', '8.1', '8.3']
services:
mysql:
image: mariadb:10.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="healthcheck.sh --connect"
--health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
- name: Instalar ambiente de testes
run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 latest
- name: Composer install
run: composer install --prefer-dist --no-progress
- name: Rodar PHPUnit
run: vendor/bin/phpunit
Teste de unidade × teste de integração
Vale fixar a distinção, porque ela orienta o que você consegue (e o que não consegue) testar com a suíte padrão:
- Unidade: testa uma função/classe em isolamento, com dependências substituídas por mocks. Rápido, mas exige código desacoplado das funções globais do WordPress.
- Integração: testa o seu código junto ao WordPress real (banco, hooks, opções). É o que a
WP_UnitTestCaseentrega por padrão — mais lento, porém muito mais fiel ao que acontece em produção.
A recomendação prática: extraia regras de negócio puras para classes que possam ser testadas em unidade, e cubra com testes de integração tudo que toca o core. Você ganha velocidade onde dá e fidelidade onde importa.
Armadilhas comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
| Dados de dev sumiram | Banco de testes apontava para banco real | Use sempre um banco dedicado (wordpress_test) |
Could not find wordpress-tests-lib | Lib de testes não instalada / WP_TESTS_DIR errada | Rode o install-wp-tests.sh e/ou exporte WP_TESTS_DIR |
| Erro de versão do PHPUnit | Incompatibilidade PHP × PHPUnit × lib do WP | Fixe versões no composer.json conforme a matriz oficial |
| Testes passam isolados, falham juntos | Estado global vazando entre testes | Limpe hooks/opções no set_up()/tear_down() |
| Teste depende da ordem de execução | Acoplamento a estado de outro teste | Cada teste deve montar seus próprios dados com a factory |
Boas práticas
- Um banco de testes inconfundível e descartável. Trate-o como volátil — ele será recriado. Em CI, isso é natural; em local, mantenha-o isolado.
- Use a factory, não
wp_insert_post()manual. Os dados ficam consistentes e o rollback cuida da limpeza. - Cada teste é independente. Não dependa de ordem nem de dados deixados por outro teste; monte o cenário no próprio método ou no
set_up(). - Nomeie testes pelo comportamento.
test_pedido_pago_dispara_email()diz mais do quetest_funcao1(). - Rode no CI com matrix. Testar em várias versões de PHP e WordPress pega incompatibilidades que o seu ambiente local esconde.
- Comece pequeno. Um punhado de testes nos caminhos críticos já paga: cubra primeiro o que, se quebrar, gera prejuízo (checkout, salvamento de dados, permissões).
Quer levar a casa? Combine os testes com comandos WP-CLI próprios para automatizar tarefas e mantenha tudo organizado dentro do seu fluxo de desenvolvimento. Testes não eliminam bugs, mas mudam o momento em que você os descobre — do cliente irritado para o pull request ainda aberto.
Perguntas frequentes
Preciso de um banco de dados separado para os testes?
Sim, e isso é inegociável. O script bin/install-wp-tests.sh instala uma cópia do WordPress e cria um banco de testes que é apagado e recriado a cada execução. Se você apontar para o banco de produção (ou de desenvolvimento com dados que importam), os dados serão destruídos. Use sempre um banco dedicado, com um nome óbvio como wordpress_test, idealmente em um MySQL local ou em container, nunca no servidor de produção.
Os testes do WordPress são de unidade ou de integração?
Na prática, a maioria é de integração. A WP_UnitTestCase carrega o WordPress inteiro, com banco, hooks e funções reais. Você testa o comportamento do seu código dentro do WordPress, não em isolamento. Testes de unidade puros (sem WordPress) são possíveis com mocks, mas exigem desacoplar a lógica das funções globais — algo que vale a pena para regras de negócio, mas raramente para código que conversa direto com o core.
Qual versão de PHPUnit devo usar?
Depende da versão de PHP e do WordPress. O ecossistema histórico do core trava o PHPUnit em versões antigas (5.x–7.x via o pacote phpunit/phpunit) por causa de compatibilidade. Em projetos modernos, prefira o pacote oficial wp-phpunit/wp-phpunit via Composer e siga a matriz de compatibilidade documentada. Conflitos entre versão de PHP, PHPUnit e a biblioteca de testes são a fonte nº 1 de erros de ambiente — fixe as versões no composer.json.
O que a factory (<code>$this->factory()</code>) faz?
É um gerador de dados de teste. $this->factory()->post->create() insere um post real no banco de testes e devolve o ID; há fábricas para usuários, termos, comentários, attachments e mais. Isso evita escrever wp_insert_post() manualmente em cada teste e mantém os dados consistentes. Como o banco sofre rollback entre os testes, os registros criados não vazam de um teste para o outro.
Por que meus testes "vazam" estado entre si?
A WP_UnitTestCase faz rollback do banco via transações, mas não reseta automaticamente todo o estado global do PHP: $GLOBALS, opções em cache, hooks que você adicionou, singletons. Se um teste adiciona um add_filter e não o remove, o próximo teste pode herdá-lo. Adicione e remova no setUp()/tearDown(), ou registre o filtro dentro do próprio teste sabendo que o core restaura os hooks ao final de cada teste.
Dá para rodar os testes no GitHub Actions?
Sim, e é a prática recomendada. Você sobe um serviço MySQL no workflow, roda o install-wp-tests.sh dentro do runner e dispara o phpunit. Use uma matrix para testar várias versões de PHP e de WordPress de uma vez. Assim, toda abertura de pull request executa a suíte e você descobre regressões antes do merge, não em produção.