Melhore a testabilidade do seu código PHP

Fabio Hiroki - Sep 25 '21 - - Dev Community

Introdução

gif de boas vindas
Bem vindo ao meu artigo! Espero que você seja como eu e acredite no poder dos testes unitários, ou pelo menos saiba que eles são importantes. Resumindo, eu gosto deles porque me dão confiança que meu código está funcionando e ninguém mais fará alterações nele por engano.

Nesse artigo eu vou mostrar como combinar mocks e reflexão para escrever um teste que a princípio parecia impossível.

O código completo está no Github.

GitHub logo fabiothiroki / php-reflection-test

A simple example of unit tests using reflection

Desafio aceito

gif escrito challenge accepted
Eu havia acabado de começar num trabalho novo, numa empresa que usa PHP como linguagem principal. Peguei uma nova tarefa e estava muito animado para terminá-la (com testes, é claro!).

Mas, de repente, uma classe selvagem aparece! Ela não expunha uma dependência que eu precisava mockar. Eu poderia refatorar esse código pra receber a dependência através do construtor, mas decidi não seguir esse caminho porque eu era novo na empresa e não sabia se isso era um padrão aceitável ou qual foi o racional da pessoa que desenvolveu a classe originalmente.

Por outro lado, a solução alternativa que eu mostrar a seguir também não demandaria um esforço grande. Vindo de um background Java, eu sabia que isso poderia ser feito usando "reflexão".

Reflexão? O que?

Antes de irmos direto para o código precisamos entender o conceito de reflexão. Aqui vai a minha definição simples:

Reflexão é uma ferramenta que permite inspecionar o código e passar por cima da tipagem estática.

Por exemplo, nesse artigo vou utilizar reflexão para para alterar a instância de um atributo inacessível, de uma classe que não tem um método "setter" nem uma outra forma de acessá-la.

Por que você quer fazer isso?

Diagrama de classes

Nesse exemplo, a classe Implementation contém um método público que faz uma chamada para a dependência inacessível. Então o teste que queremos fazer é: quando chamamos o método público de Implementation, estamos chamando a dependência com os argumentos corretos?

Me mostre o código

Classe abstrata

<?php
abstract class AbstractClass
{
    protected $unacessibleDependency;

    public function __construct()
    {
        $this->unacessibleDependency = new UnacessibleDependency();
    }

    abstract function doSomething(string $argument): void;
}
Enter fullscreen mode Exit fullscreen mode

Como podemos observar, não há uma forma de acessar $unacessibleDependency externamente, a não ser que refatoremos essa classe, o que já disse anteriormente que não é o caminho que decidi não seguir.

Classe Implementation

<?php
class ImplementationClass extends AbstractClass
{
    public function doSomething(string $argument): void
    {
        $this->unacessibleDependency->doSomethingElse($argument);
    }
}
Enter fullscreen mode Exit fullscreen mode

doSomething é o método que queremos testar.

Configuração do teste

Comece criando uma instância do sistema a ser testado (system under test ou SUT):

$this->implementation = new ImplementationClass();
Enter fullscreen mode Exit fullscreen mode

Torne a unacessibleDependency da instância anterior acessível por meio da reflexão:

$reflectionClass = new ReflectionClass($this->implementation);
$property = $reflectionClass->getProperty('unacessibleDependency');
$property->setAccessible(true);
Enter fullscreen mode Exit fullscreen mode

Crie uma instância mock de UnacessibleDependency para que possamos verificar se está sendo chamada:

$this->mockUnacessible = $this->createMock(UnacessibleDependency::class);
Enter fullscreen mode Exit fullscreen mode

Então, substitua a dependência original pelo object mock:

$property->setValue($this->implementation, $this->mockUnacessible);
Enter fullscreen mode Exit fullscreen mode

Por fim, a afirmação

$this->mockUnacessible->expects(self::once())
            ->method('doSomethingElse')
            ->with(self::equalTo('argument'));

$this->implementation->doSomething('argument');
Enter fullscreen mode Exit fullscreen mode

Traduzindo para linguagem humana: quando doSomething é chamado com o parâmetro argument, doSomethingElse é também chamado com o mesmo parâmetro?

Agora nós temos nosso teste, e ninguém se feriu durante esse processo.

gif de celebração

Conclusão

Existe uma forma de adicionar um teste unitário mesmo em cenários improváveis, mas essa técnica deve ser usado com cuidado porque nós podemos estar mascarando um design de código ruim. Se o código não está facilmente testável, então provavelmente não estamos seguindo as boas práticas de encapsulamento ou abstração.

Eu quero saber mais!

Para mais técnicas de melhoria de testabilidade de código, dê uma olhada no livro "Trabalho Eficaz com Código Legado" de Michael Feathers.

. . . . . . . . . . . . . . . . . . . . .