Testes unitários – lidar com dependencies

Isso pode ser visto como um corolário do retorno de chamada de testes de ganchos .

O problema: eu quero testar uma class que cria uma nova instância de uma class My_Notice definida fora do plugin (vamos chamá-lo de “Plugin Principal”).

Meu teste de unidade não conhece nada sobre My_Notice porque está definido em uma biblioteca de terceiros (outro plugin, para ser preciso). Portanto, eu tenho essas opções (tanto quanto eu sei):

  1. Stub the My_Notice class: difícil de manter
  2. Inclua os arquivos necessários da biblioteca de terceiros: isso pode funcionar, mas estou fazendo meus testes menos isolados
  3. Dinamicamente stub a class: não tenho certeza se isso é possível, mas seria muito semelhante a zombar de uma class, exceto que também deveria criar a mesma definição, então a class que estou testando será capaz de instanciá-la.

A resposta de @gmazzap aponta que devemos evitar criar esse tipo de dependencies, algo que eu concordo totalmente. E a idéia de criar classs de stub não me parece boa: prefiro include o código do “Plugin principal”, com todas as conseqüências).

No entanto, não vejo como posso fazer o contrário.

Aqui está um exemplo do código que estou tentando testar:

 class My_Admin_Notices_Handler { public function __construct( My_Notices $admin_notices ) { $this->admin_notices = $admin_notices; } /** * This will be hooked to the `activated_plugin` action * * @param string $plugin * @param bool $network_wide */ public function activated_plugin( $plugin, $network_wide ) { $this->add_notice( 'plugin', 'activated', $plugin ); } /** * @param string $type * @param string $action * @param string $plugin */ private function add_notice( $type, $action, $plugin ) { $message = ''; if ( 'activated' === $action ) { if ( 'plugin' === $type ) { $message = __( '%1s Some message for plugin(s)', 'my-test-domain' ); } if ( 'theme' === $type ) { $message = __( '%1s Some message for the theme', 'my-test-domain' ); } } if ( 'updated' === $action && ( 'plugin' === $type || 'theme' === $type ) ) { $message = __( '%1s Another message for updated theme or plugin(s)', 'my-test-domain' ); } if ( $message ) { $notice = new My_Notice( $plugin, 'wpml-st-string-scan' ); $notice->text = $message; $notice->actions = array( new My_Admin_Notice_Action( __( 'Scan now', 'my-test-domain' ), '#' ), new My_Admin_Notice_Action( __( 'Skip', 'my-test-domain' ), '#', true ), ); $this->admin_notices->add_notice( $notice ); } } } 

Basicamente, esta class possui um método que será conectado a activated_plugin . Esse método cria uma instância de uma class de “notificação”, que será armazenada em algum lugar pela instância My_Notices passada para o construtor.

O construtor da class My_Notice recebe dois argumentos básicos (um UID e um “grupo”) e obtém algumas propriedades definidas (tenha em mente que o mesmo problema é com a class My_Admin_Notice_Action ).

Como eu poderia fazer a class My_Notice uma dependência injetada?

Claro, eu poderia usar uma matriz associativa, chamar uma ação, que é enganchada pelo “Plugin Principal” e que traduz essa matriz nos argumentos da class, mas ela não parece limpa para mim.

Solutions Collecting From Web of "Testes unitários – lidar com dependencies"

A injeção de objects é realmente necessária?

Para ser totalmente testável de forma isolada, o código deve evitar instanciar as classs diretamente e injetar qualquer object que seja necessário dentro dos objects.

No entanto, existem pelo menos duas exceções a esta regra:

  1. A class é uma class de idioma, por exemplo ArrayObject
  2. A class é um “object de valor” apropriado.

Não há necessidade de forçar injeção para objects do núcleo

O primeiro caso é fácil de explicar: é algo embutido no idioma, então você apenas assume que funciona. Caso contrário, você também deve testar todas as funções ou instruções básicas do PHP como return

Não há necessidade de forçar injeção para object de valor

O segundo caso, está relacionado à natureza de um object de valor. Em fatos:

  • é imutável
  • não tem nenhuma alternativa polimórfica possível
  • por definição, uma instância de object de valor é indistinguível de outra que possui argumentos do mesmo construtor

Isso significa que um object de valor pode ser visto como um tipo imutável por conta própria, como, por exemplo, uma string.

Se algum código faz $myEmail = 'some.email@example.com' ninguém está preocupado em zombar dessa string, e da mesma forma, ninguém deve se preocupar em zombar de uma linha como new Email('some_name@example.com') (assumindo que o e- Email é imutável e possivelmente final ).

Para o que posso adivinhar do seu código, My_Admin_Notice_Action é um bom candidato para ser / tornar-se um object de valor. Mas não pode ter certeza sem ver o código.

Não posso dizer o mesmo de My_Notice , mas isso é apenas mais um palpite.

Se for necessário injetar …

Caso a class que seja instanciada em outra class não seja um dos dois casos acima, certamente é melhor injetá-lo.

No entanto, como exemplo em OP, a class a ser construída precisa de argumentos que dependem do contexto.

Não existe uma “única” resposta neste caso, mas abordagens diferentes que podem ser válidas dependendo do caso.

Instanciação no código do cliente

Uma abordagem simples é separar “código de objects” do “código do cliente”. Onde o código do cliente é o código que faz uso de objects.

Desta forma, você pode testar o código dos objects usando os testes de unidade e deixar o teste do código do cliente para testes funcionais / de integração, onde você não precisa se preocupar com a isolação.

No seu caso, será algo como:

 add_action( 'activate_plugin', function( $plugin, $network_wide ) { $message = My_Admin_Notices_Message( 'plugin', 'activated' ); if ( $message->has_text() ) { $notice = new My_Notice( $plugin, $message, 'wpml-st-string-scan' ); $notice->add_action( new My_Admin_Notice_Action( 'Scan now', '#' ) ); $notice->add_action( new My_Admin_Notice_Action( 'Skip', '#', true ) ); $notices = new My_Notices(); $notices->add_notice( $notice ); $handler = new My_Admin_Notices_Handler( $notices ); $handler->handle_notices(); } }, 10, 2); 

Eu fiz algumas suposições em seu código e escreve methods e classs que podem não existir (como My_Admin_Notices_Message ), mas o ponto aqui é que o encerramento acima contém todos os códigos do cliente necessários para instanciar e “usar” os objects. Você pode então testar seus objects isoladamente porque nenhum desses objects precisa instanciar outros objects, mas todos eles recebem instâncias necessárias no construtor ou como methods params.

Simplifique o código do cliente com as fábricas

A abordagem acima pode funcionar bem para pequenos plugins (ou pequena parte de um plugin que pode ser isolado do resto), mas para bases de código maiores, usando apenas essa abordagem, você pode terminar em código de cliente grande em fechamentos que, entre outros coisas, é muito difícil de testar e manter.

Nesses casos, as fábricas podem ajudá-lo. As fábricas são objects com o único âmbito de criação de outros objects. Na maioria das vezes é bom ter fábricas específicas para objects do mesmo tipo (implementando a mesma interface).

Com fábricas, o código acima pode ser assim:

 add_action( 'activate_plugin', function( $plugin, $network_wide ) { $notice = $notice_factory->create_for( 'plugin', 'activated' ); if ( $notice instanceof My_Notice_Interface ) { $handler_factory = new My_Admin_Notices_Handler_Factory(); $handler = $handler_factory->build_for_notices( [ $notice ] ); $handler->handle_notices(); } }, 10, 2); 

Todo o código de instanciação está em fábricas. Você ainda pode testar fábricas de forma isolada, porque você precisa testar que os argumentos corretos fornecidos produzem aulas esperadas (ou que os argumentos errados que eles produzem erros esperados).

E ainda pode testar todos os outros objects de forma isolada porque nenhum object precisa criar instâncias, o código de instanciação de fatos é tudo em fábricas.

Claro, lembre-se de que os objects de valor não precisam de fábricas … seria como criar fábricas para strings …

E se eu não puder alterar o código?

Stubs

Às vezes, não é possível alterar o código que instancia outros objects, por diferentes motivos. Por exemplo, o código é o terceiro, a compatibilidade com versões anteriores e assim por diante.

Nesses casos, se for possível executar os testes sem carregar as classs sendo instanciadas, você pode escrever alguns talões.

Vamos assumir que você tem um código que faz:

 class Foo { public function run_something() { $something = new Something(); $something->run(); } } 

Se você puder executar testes sem carregar a class Something , você pode escrever uma class Custom personalizada apenas com o objective de testar (um “stub”).

É sempre melhor manter os talões muito simples, por exemplo:

 class Something{ public function run() { return 'I ran'. } } 

Quando os testes são executados, você pode então carregar o arquivo que contém este stub para Something class (por exemplo, de testes setUp() ) e quando a class em testes instanciará um new Something , você o testará de forma isolada, uma vez que a configuração é muito simples e você pode criá-lo de uma forma que, por design, faz o que você espera.

Claro que isso não é muito simples de manter, mas considerando que normalmente você não faz teste de código de terceiros, raramente você precisa fazer isso.

Às vezes, porém, isso é útil para testar o código de plugins / temas de isolamento que instanciam os objects do WordPress (por exemplo, WP_Post ).

Sobrecarga de zombaria

Usando Mockery (uma biblioteca que fornece ferramentas para testes de unidade PHP), você pode até mesmo evitar escrever esses talões. Com a Mockery “instância simulada” (também conhecido como “sobrecarga”) é possível interceptar novas instâncias de criação e replace com uma simulação. Este artigo explica muito bem como fazê-lo.

Quando a class está carregada …

Se o código a testar tiver dependencies rígidas (instanciar classs usando new ) e não há possibilidade de carregar testes sem carregar a class que será instanciada, há chances muito pequenas de testá-lo isoladamente sem tocar o código (ou escrever um camada de abstração ao redor).

No entanto, note que o arquivo de boot de teste é frequentemente carregado como primeira coisa absoluta. Então, existem pelo menos dois casos em que você pode forçar o carregamento de seus talões:

  1. O código usa um carregador automático. Neste caso, se você carregar os stubs antes de carregar o carregador automático, então a class “real” nunca será carregada, porque quando new é usado, a class já foi encontrada e o carregador automático não foi acionado.

  2. As verificações de código para class_exists antes de definir / carregar a class. Nesse caso, para carregar os talões como primeira coisa, impedirá que a class “real” seja carregada.

Último último complicado

Quando tudo o resto falhar, há outra coisa que você pode fazer para testar dependencies difíceis.

Muitas vezes, dependencies rígidas são armazenadas como variables ​​privadas.

 class Foo { public function __construct() { $this->something = new Something(); } public function run_something() { return $this->something->run(); } } 

Em casos como este, você pode replace as dependencies rígidas por uma mock / stub após a criação da instância.

Isso porque mesmo private propriedades private podem ser (bastante) facilmente substituídas no PHP. De mais de uma maneira, na verdade.

Sem cavar em detalhes, posso dizer que a binding de fechamento pode ser usada para fazer isso. Eu até escrevi uma biblioteca chamada “Andrew” que pode ser usada para o escopo.

Usando Andrew (e Mockery) para testar a class Foo acima, você pode fazer:

 public function test_run_something() { $foo = new Foo(); $something_mock = Mockery::mock( 'Something' ); $something_mock ->shouldReceive('run') ->once() ->withNoArgs() ->andReturn('I ran.'); // changing proxy properties will change private properties of proxied object $proxy = new Andrew\Proxy( $foo ); $proxy->something = $something_mock; $this->assertSame( 'I ran.', $foo->run_something() ); }