Melhore a testabilidade do seu código PHP
Tags: braziliandevs, webdev, tutorial, php
Published: 2021-09-25 18:17:33

## Introdução
![gif de boas vindas](https://media.giphy.com/media/l0MYC0LajbaPoEADu/giphy.gif?cid=ecf05e47mubvjwga5b9ynfv1d66s8dl7n27n0dwfagdngqvb&rid=giphy.gif&ct=g)
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](https://github.com/fabiothiroki/php-reflection-test).

{% github fabiothiroki/php-reflection-test no-readme %} 

## Desafio aceito
![gif escrito challenge accepted](https://media.giphy.com/media/QxZ0nbcVgMlPlnfZos/giphy.gif?cid=ecf05e47mq4dg5zivdkvqglw6fu1m08j12nqyzx7s8g7lvop&rid=giphy.gif&ct=g)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2kstaqojvbeblfd66izs.png) 

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
<?php
abstract class AbstractClass
{
    protected $unacessibleDependency;

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

    abstract function doSomething(string $argument): void;
}
```

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
<?php
class ImplementationClass extends AbstractClass
{
    public function doSomething(string $argument): void
    {
        $this->unacessibleDependency->doSomethingElse($argument);
    }
}
```
`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):
```php
$this->implementation = new ImplementationClass();
```
Torne a `unacessibleDependency` da instância anterior acessível por meio da reflexão:
```php
$reflectionClass = new ReflectionClass($this->implementation);
$property = $reflectionClass->getProperty('unacessibleDependency');
$property->setAccessible(true);
```
Crie uma instância mock de `UnacessibleDependency` para que possamos verificar se está sendo chamada:
```php
$this->mockUnacessible = $this->createMock(UnacessibleDependency::class);
```
Então, substitua a dependência original pelo object mock:
```php
$property->setValue($this->implementation, $this->mockUnacessible);
```
### Por fim, a afirmação
```php
$this->mockUnacessible->expects(self::once())
            ->method('doSomethingElse')
            ->with(self::equalTo('argument'));

$this->implementation->doSomething('argument');
```
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](https://media.giphy.com/media/IwAZ6dvvvaTtdI8SD5/giphy.gif?cid=ecf05e47g4t1sip0uq1bck5ttucowfbi5zkzbhc9br08dhd0&rid=giphy.gif&ct=g)

## 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.