Teste de retorno de chamada de ganchos

34

Estou desenvolvendo um plugin usando TDD e uma coisa que eu falho completamente ao testar são ... ganchos.

Quero dizer OK, posso testar o retorno de chamada do gancho, mas como eu poderia testar se um gancho realmente é acionado (ganchos personalizados e ganchos padrão do WordPress)? Suponho que algumas zombarias ajudem, mas simplesmente não consigo descobrir o que estou perdendo.

Eu instalei o conjunto de testes com o WP-CLI. De acordo com esta resposta , o initgancho deve ser acionado, mas ... não é; Além disso, o código funciona dentro do WordPress.

Pelo que entendi, o bootstrap é carregado por último, então faz sentido não acionar o init, então a questão que permanece é: como diabos devo testar se os ganchos são acionados?

Obrigado!

O arquivo de inicialização aparece assim:

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
  require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

O arquivo testado fica assim:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

E o teste em si:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Obrigado!

Ionut Staicu
fonte
Se você estiver executando phpunit, consegue ver os testes reprovados ou com falha? Você instalou bin/install-wp-tests.sh?
Sven
Acho que parte do problema é que talvez RegisterCustomPostType::__construct()nunca seja chamado quando o plug-in é carregado para os testes. Também é possível que você esteja sendo afetado pelo bug # 29827 ; talvez tente atualizar sua versão do conjunto de testes de unidade do WP.
JD
@ Sven: sim, os testes estão falhando; i instalada bin/install-wp-tests.sh(desde que eu usei wp-cli) @JD: RegisterCustomPostType :: __ construct é chamado (apenas acrescentou uma die()declaração e phpunit pára por aí)
Ionut Staicu
Não tenho muita certeza do lado dos testes de unidade (não é o meu forte), mas do ponto de vista literal, você pode usar did_action()para verificar se as ações foram acionadas.
Rarst
@Rarst: obrigado pela sugestão, mas ainda não funciona. Por alguma razão, acho que o momento está errado (os testes são executados antes do initgancho).
Ionut Staicu

Respostas:

72

Teste isolado

Ao desenvolver um plugin, a melhor maneira de testá-lo é sem carregar o ambiente WordPress.

Se você escrever um código que possa ser facilmente testado sem o WordPress, seu código se tornará melhor .

Todo componente que é testado em unidade deve ser testado isoladamente : ao testar uma classe, você só precisa testar essa classe específica, assumindo que todos os outros códigos estejam funcionando perfeitamente.

O Isolador

Esta é a razão pela qual os testes de unidade são chamados de "unidade".

Como um benefício adicional, sem carregar o núcleo, seu teste será executado muito mais rápido.

Evite ganchos no construtor

Uma dica que posso lhe dar é evitar colocar ganchos nos construtores. Essa é uma das coisas que tornará seu código testável isoladamente.

Vamos ver o código de teste no OP:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

E vamos supor que esse teste falhe . Quem é o culpado ?

  • o gancho não foi adicionado ou não está corretamente?
  • o método que registra o tipo de postagem não foi chamado de todo ou com argumentos errados?
  • existe um bug no WordPress?

Como pode ser melhorado?

Vamos supor que seu código de classe seja:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

(Nota: vou me referir a esta versão da turma pelo restante da resposta)

A maneira como escrevi essa classe permite criar instâncias da classe sem chamar add_action.

Na classe acima, há duas coisas a serem testadas:

  • o método init realmente chama add_actionpassando a ele argumentos adequados
  • o método register_post_type realmente chama register_post_typefunção

Eu não disse que você precisa verificar se o tipo de postagem existe: se você adicionar a ação apropriada e se ligar register_post_type, o tipo de postagem personalizado deverá existir: se não existir, será um problema no WordPress.

Lembre-se: ao testar seu plug-in, você deve testar seu código, não o código do WordPress. Nos seus testes, você deve assumir que o WordPress (como qualquer outra biblioteca externa que você usa) funciona bem. Esse é o significado do teste de unidade .

Mas ... na prática?

Se o WordPress não estiver carregado, se você tentar chamar os métodos de classe acima, você receberá um erro fatal e precisará zombar das funções.

O método "manual"

Claro que você pode escrever sua biblioteca de zombaria ou zombar "manualmente" de todos os métodos. É possível. Vou lhe dizer como fazer isso, mas depois mostrarei um método mais fácil.

Se o WordPress não for carregado enquanto os testes estiverem sendo executados, significa que você pode redefinir suas funções, por exemplo, add_actionou register_post_type.

Vamos supor que você tenha um arquivo carregado a partir do seu arquivo de inicialização, onde você tem:

function add_action() {
  global $counter;
  if ( ! isset($counter['add_action']) ) {
    $counter['add_action'] = array();
  }
  $counter['add_action'][] = func_get_args();
}

function register_post_type() {
  global $counter;
  if ( ! isset($counter['register_post_type']) ) {
    $counter['register_post_type'] = array();
  }
  $counter['register_post_type'][] = func_get_args();
}

Reescrevi as funções para simplesmente adicionar um elemento a uma matriz global toda vez que elas são chamadas.

Agora você deve criar (se ainda não tiver um) sua própria classe de caso de teste de base estendendo PHPUnit_Framework_TestCase: isso permite que você configure facilmente seus testes.

Pode ser algo como:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

Dessa forma, antes de cada teste, o contador global é redefinido.

E agora seu código de teste (refiro-me à classe reescrita que publiquei acima):

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->init();
     $this->assertSame(
       $counter['add_action'][0],
       array( 'init', array( $r, 'register_post_type' ) )
     );
  }

  function test_register_post_type() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->register_post_type();
     $this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
  }

}

Você deve observar:

  • Consegui chamar os dois métodos separadamente e o WordPress não é carregado. Dessa forma, se um teste falhar, eu sei exatamente quem é o culpado.
  • Como eu disse, aqui testo que as classes chamam funções WP com argumentos esperados. Não há necessidade de testar se o CPT realmente existe. Se você está testando a existência do CPT, está testando o comportamento do WordPress, não o comportamento do seu plug-in ...

Bom .. mas é uma PITA!

Sim, se você precisar zombar manualmente de todas as funções do WordPress, é realmente uma dor. Um conselho geral que posso dar é usar o menor número possível de funções WP: você não precisa reescrever o WordPress, mas funções abstratas do WP que você usa em classes personalizadas, para que possam ser ridicularizadas e facilmente testadas.

Por exemplo, no exemplo acima, você pode escrever uma classe que registra os tipos de postagem, chamando register_post_type'init' com os argumentos fornecidos. Com essa abstração, você ainda precisa testar essa classe, mas em outros locais do seu código que registram tipos de postagem, você pode fazer uso dessa classe, zombando dela em testes (supondo que ela funcione).

O mais impressionante é que, se você escrever uma classe que abstraia o registro do CPT, poderá criar um repositório separado para ela e, graças a ferramentas modernas como o Composer, ele será incorporado em todos os projetos onde você precisar: teste uma vez, use em qualquer lugar . E se você encontrar um bug nele, poderá corrigi-lo em um só lugar e com um simples composer updatetodos os projetos em que ele é usado também serão corrigidos.

Pela segunda vez: escrever código testável isoladamente significa escrever código melhor.

Mas mais cedo ou mais tarde eu preciso usar as funções do WP em algum lugar ...

Claro. Você nunca deve agir paralelamente ao núcleo, não faz sentido. Você pode escrever classes que agrupam as funções do WP, mas essas classes também precisam ser testadas. O método "manual" descrito acima pode ser usado para tarefas muito simples, mas quando uma classe contém muitas funções do WP, pode ser uma dor.

Felizmente, lá existem pessoas boas que escrevem coisas boas. A 10up , uma das maiores agências de WP, mantém uma biblioteca muito boa para pessoas que desejam testar plugins da maneira certa. É WP_Mock.

Ele permite que você zombe das funções do WP e ganchos . Supondo que você tenha carregado em seus testes (consulte o repositório leia-me), o mesmo teste que escrevi acima se torna:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     \WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     \WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

Simples, não é? Esta resposta não é um tutorial para WP_Mock, então leia o readme do repositório para obter mais informações, mas o exemplo acima deve ser bem claro, eu acho.

Além disso, você não precisa escrever nenhuma zombaria add_actionou register_post_typesozinho, nem manter nenhuma variável global.

E aulas WP?

O WP também tem algumas classes e, se o WordPress não estiver carregado ao executar testes, você precisará zombar delas.

Isso é muito mais fácil do que zombar de funções, o PHPUnit possui um sistema incorporado para zombar de objetos, mas aqui quero sugerir o Mockery para você. É uma biblioteca muito poderosa e muito fácil de usar. Além disso, é uma dependência de WP_Mock, portanto, se você o tiver, também terá Zombaria.

Mas que tal WP_UnitTestCase?

O conjunto de testes do WordPress foi criado para testar o núcleo do WordPress e, se você quiser contribuir com o núcleo, é essencial, mas usá-lo para plug-ins faz com que você não teste isoladamente.

Coloque seus olhos no mundo do WP: existem muitas estruturas PHP modernas e CMS por aí e nenhuma delas sugere testar plugins / módulos / extensões (ou como eles são chamados) usando o código da estrutura.

Se você sente falta de fábricas, um recurso útil da suíte, precisa saber que há coisas incríveis por lá.

Pegadinhas e desvantagens

Há um caso em que o fluxo de trabalho que sugeri aqui não possui: teste de banco de dados personalizado .

De fato, se você usar tabelas e funções padrão do WordPress para escrever lá (nos $wpdbmétodos de nível mais baixo ), nunca precisará realmente escrever dados ou testar se os dados estão realmente no banco de dados, apenas certifique-se de que os métodos adequados sejam chamados com argumentos adequados.

No entanto, você pode escrever plug-ins com tabelas e funções personalizadas que criam consultas para serem escritas lá e testar se essas consultas funcionam, é de sua responsabilidade.

Nesses casos, a suíte de testes do WordPress pode ajudá-lo muito, e o carregamento do WordPress pode ser necessário em alguns casos para executar funções como dbDelta.

(Não é necessário dizer para usar um db diferente para testes, não é?)

Felizmente, o PHPUnit permite que você organize seus testes em "suítes" que podem ser executadas separadamente, para que você possa criar um conjunto para testes de banco de dados personalizados, onde você carrega o ambiente WordPress (ou parte dele), deixando todo o restante dos testes sem WordPress .

Apenas certifique-se de escrever classes que abstraiam o maior número possível de operações de banco de dados, de maneira que todas as outras classes de plug-ins as utilizem, para que, usando o mock, você possa testar adequadamente a maioria das classes sem lidar com o banco de dados.

Pela terceira vez, escrever código facilmente testável isoladamente significa escrever código melhor.

gmazzap
fonte
5
Caramba, muita informação útil! Obrigado! De alguma forma, eu consegui perder todo o objetivo dos testes de unidade (até agora, eu estava praticando testes de PHP apenas dentro do Code Dojo). Também descobri o wp_mock hoje cedo, mas por algum motivo eu consigo ignorá-lo. O que me irritou é que qualquer teste, por menor que fosse, levava pelo menos dois segundos para ser executado (carregue o WP primeiro, execute o segundo). Obrigado novamente por abrir meus olhos!
Ionut Staicu
4
Obrigado @IonutStaicu Eu esqueci de mencionar que o não carregamento do WordPress faz seus testes muito mais rápidos
gmazzap
6
Também vale ressaltar que a estrutura de teste de unidade WP Core é uma ferramenta incrível para executar testes de INTEGRATION, que seriam testes automatizados para garantir que se integre bem ao próprio WP (por exemplo, não há colisão acidental de nomes de funções, etc.).
John P Bloch
1
@JohnPBloch +1 para um bom argumento. Mesmo que o uso de um espaço para nome seja suficiente para evitar qualquer colisão de nomes de função no WordPress, onde tudo é global :) Mas, com certeza, integrações / testes funcionais são uma coisa. Eu estou tocando com o Behat + Mink no momento, mas ainda estou praticando com isso.
gmazzap
1
Agradecimentos para o "passeio de helicóptero" sobre a floresta UnitTest de WordPress - Eu ainda estou rindo de que a imagem épica ;-)
birgire