Test doubles
Tags: womenintech, testing, braziliandevs, php
Published: 2021-06-07 17:48:59

- [Recorrendo a Test Doubles](#recorrendo-test-doubles)
- [Quais Test Doubles existem?](#quais-existem)
- [Test Doubles e Refatoração](#refatoracao)
- [Cuidado com o SUT](#cuidado-sut)

----

Olá olá!

Hoje eu quero falar de algo que me confundiu muito quando comecei a aplicar testes: como eu faço pra substituir coisas desnecessárias ao objetivo do teste? Quais conceitos preciso entender para escrever os testes de forma que meus pares também entendam?

Imagine que você está desenvolvendo uma integração com algum meio de pagamento (PayPal, Mercado Pago, etc.) e precisa testar essa classe.

Podemos criar os testes sem alterar nada no código e continuar chamando a integração. Fazer isso vem com vários problemas como criar uma dependência do nosso teste no serviço externo, aumentar o tempo de execução da rotina de testes, pode afetar algum limite de requisição que o serviço imponha, e por ai vai...

Podemos criar nossos testes ignorando todas as etapas que utilizam o serviço externo, mas assim vamos acabar ignorando parte da lógica interna. Como testar se o banco de dados está sendo atualizado depois que o pagamento foi feito com sucesso? Como confirmar que os tratamentos de erro estão funcionando conforme o esperado?

## Recorrendo a Test Doubles <a name="recorrendo-test-doubles"></a>

O termo *Test Doubles* foi descrito no livro *XUnit Test Patterns: Refactoring Test Code*, de Gerard Meszaros e **representa qualquer objeto que simula um objeto real para fins de teste**. 

Quando escrevemos nossos testes e nos deparamos com casos onde não podemos usar um componente real ou casos onde não seria prático usá-lo, podemos substituí-lo por um *Test Double*. Os *test doubles* não precisam ser exatamente como o componente real, precisam apenas se comportar de tal forma que o sistema sobre teste (*System Under Test* ou SUT) pense que ele é real.

**Vamos para um exemplo prático?** 

Precisamos fazer um sistema de assinatura que pode ter vários meios de pagamento. 

Uma forma de executar isso é criando uma classe de integração para cada meio de pagamento e padronizá-las através de uma interface, chamada `Gateway`, para garantir que todas as classes respeitem o mesmo contrato.

Esse contrato é dividido em três funções: `register`, que cria um novo registro do nosso usuário na plataforma de pagamento, `activate`, que ativa a assinatura, e `deactivate`, que inativa a assinatura quando o usuário quiser cancelar seu plano:

```php
<?php

namespace App;

interface Gateway
{
    public function register(string $email): void;

    public function activate(): string;

    public function deactivate(): void;
}
```

Com a interface `Gateway` padronizamos como será feita a integração com o meio de pagamento, mas agora precisamos resolver as nossas regras de negócio.

Para isso temos a classe `Subscription` que será responsável por chamar a integração, o gerenciamento do usuário e enviar um email de confirmação da assinatura. Isso tudo será feito dentro de uma função chamada `create`:

```php
<?php

namespace App;

class Subscription
{
    protected Gateway $gateway;
    protected Mailer $mailer;

    public function __construct(Gateway $gateway, Mailer $mailer)
    {
        $this->gateway = $gateway;
        $this->mailer = $mailer;
    }

    public function create(User $user): void
    {
        $this->gateway->register($user->email);

        $receipt = $this->gateway->activate();

        $user->subscribe($receipt);

        $this->mailer->send($user,'Your receipt number is: ' . $receipt);
    }
}
```

Temos também a classe `User` com todos os métodos responsáveis por gerir os dados do usuário do nosso sistema. Essa classe possui o método `subscribe`, que é responsável por vincular o usuário ao ponteiro do meio de pagamento, e o método `isSubscribed`, que retorna se aquele objeto possui ou não uma assinatura vinculada.

```php
<?php

namespace App;

class User
{
    public string $email;
    public ?string $subscription;

    public function __construct(string $email)
    {
        $this->email = $email;
        $this->subscription = null;
    }

    public function subscribe(string $subscriptionId): void
    {
        $this->subscription = $subscriptionId;
    }

    public function isSubscribed(): bool
    {
        return is_string($this->subscription);
    }
}
```

Por fim, criamos uma integração com algum serviço de envio de email chamada `Mailer` para enviar uma mensagem ao usuário:

```php
<?php

namespace App;

class Mailer
{
    public function send(User $user, string $message): void
    {
        // logica
    }
}
```

Pronto, esse é o nosso SUT, o sistema que vamos testar. E sem mais delongas, vamos aos *test doubles*!

## Quais Test Doubles existem? <a name="quais-existem"></a>

Segundo Gerard Meszaros eles podem ser divididos em cinco categorias: Fakes, Dummies, Stubs, Mocks e Spies.

### Fake

Fakes são objetos com implementações funcionais que normalmente usam atalhos para executar ações.

Para não precisar usar a implementação de produção no nosso teste, podemos criar uma classe Fake que respeita o mesmo contrato da classe de produção e usar ela nos nossos testes. Para o nosso exemplo poderia ser uma classe assim:

```php
<?php

namespace Tests;

use App\Gateway;

class GatewayFake implements Gateway
{
    public function register(string $email): void
    {
        //
    }

    public function activate(): string
    {
        return 'some-uuid';
    }

    public function deactivate(): void
    {
        //
    }
}
```

Ela usa a mesma interface de produção (`Gateway`), possui os mesmos métodos que são chamados pela classe `Subscription`, mas não executam nenhuma ação real, possui apenas o suficiente para rodarmos nossos testes.

### Dummy

Dummy é um objeto sem nenhuma funcionalidade específica; é usado apenas para preencher parâmetros.

Vamos para o primeiro caso de teste: precisamos validar se o usuário foi atualizado após a ativação da assinatura.

Usando a biblioteca PHPUnit é possível escrever o teste da seguinte forma:

```php
public function testValidaUsuarioComAssinatura(): void
{
    $gatewayFake = new GatewayFake();
    $mailerDummy = $this->createMock(Mailer::class);

    $subscription = new Subscription($gatewayFake, $mailerDummy);
    $user = new User('Maria');

    self::assertFalse($user->isSubscribed());

    $subscription->create($user);

    self::assertTrue($user->isSubscribed());
}
```

Para não chamar a integração com o meio de pagamento usamos a classe `GatewayFake` que criamos anteriormente. Como também não precisamos validar nenhum comportamento do envio de email substituímos a classe `Mailer` por um Dummy criado através do PHPUnit apenas para preencher os requisitos da construção da classe `Subscription`.

O uso de Fake e Dummy aqui foi apenas uma forma de exemplificar esses dois conceitos. Poderíamos criar um Fake para o envio de email ou um Dummy para a integração com o meio de pagamentos sem problema algum.

> 💡 Note que o nome da função chamada para criar o Dummy é `createMock(...)`. Não é incomum o uso do termo Mock como sinônimo de qualquer um dos outros *test doubles* mas ele também tem outra definição, que veremos logo a seguir.

### Stub

Stubs são objetos que usamos para simular **interações de chegada** de alguma dependência externa ao SUT. Eles são usados apenas para fornecer uma resposta programada para aquele teste.

No nosso exemplo a classe de `Subscription` solicita a ativação da assinatura através da função `activate` e atualiza o UUID que retornou da integração nos dados do usuário interno.

Para garantir a integridade dos dados do nosso usuário precisamos ter certeza que o valor retornado pela função `activate` é o mesmo que está sendo salvo no usuário:

```php
public function testValidaValorCorretoAssinatura(): void
{
    $gatewayStub = $this->createStub(Gateway::class);
    $gatewayStub->method('activate')->willReturn('some-uuid');

    $mailerDummy = $this->createMock(Mailer::class);

    $subscription = new Subscription($gatewayStub, $mailerDummy);
    $user = new User('Maria');

    // valida que o usuario foi criado sem assinatura
    self::assertNull($user->subscription);

    $subscription->create($user);

    // valida que o valor salvo no banco é o mesmo retornado pela integração
    self::assertEquals('some-uuid', $user->subscription);
}
```

Para validar esse cenário precisamos que o método `activate` retorne uma string correspondente ao UUID externo, portanto criamos um Stub para simular essa **interação de chegada**.

Como não precisamos validar o envio de email, permanecemos com o Dummy para a classe `Mailer`.

### Mock

Mocks, por sua vez, são objetos que usamos para simular **interações de saída**. Eles possuem expectativas sobre seu comportamento, ou seja, verificam se um ou mais métodos foram chamados, a ordem e quantidade de vezes que foram chamados, etc, lançando exceções quando o teste não se comporta conforme o esperado.

Vamos testar se o SUT está enviando o email quando a assinatura é criada:

```php
public function testValidaEnvioDeEmail(): void
{
    $this->expectNotToPerformAssertions();

    $user = new User('Maria');

    $gatewayStub = $this->createStub(Gateway::class);
    $gatewayStub->method('activate')->willReturn('some-uuid');

    $mailerMock = Mockery::mock(Mailer::class);
    $mailerMock
        ->shouldReceive('send')
        ->once()
        ->withArgs([$user, 'Your receipt number is: some-uuid']);

    $subscription = new Subscription($gatewayStub, $mailerMock);
    $subscription->create($user);
}
```

> Aqui acabei usando a biblioteca Mockery junto ao PHPUnit para melhorar a legibilidade do código.

Podemos ver no teste acima que logo no começo foi explicito que não haveria nenhum afirmação (`assert`). Ao invés disso apenas validamos as expectativas da **interação de saída** do SUT em relação ao serviço externo.

Ou seja, descrevemos nossa expectativa (`shouldReceive()`) que o serviço de envio de email chamaria o método `send`, que essa chamada aconteceria apenas uma vez (`once()`) e que os argumentos passados para o método (`withArgs()`) seriam o usuário e aquela exata mensagem.

Caso a mensagem ou o objeto `$user` estejam diferentes do definido no teste, o mesmo iria falhar.

### Spy

Spy pode ser definido como uma combinação do Stub com o Mock. Spies validam informações com base em como foram chamados e também permitem testar as interações tanto de chegada quanto de saída.

Diferem dos Stubs e Mocks no fato de que os Spies registram qualquer interação entre ele e o SUT e nos permitem fazer afirmações contra essas interações **após o fato**.

Usaremos um Spy para garantir que a assinatura foi ativada e não houve nenhuma chamada do fluxo de inativação:

```php
public function testConfirmaAssinaturaAtiva(): void
{
    $this->expectNotToPerformAssertions();

    $gatewaySpy = Mockery::spy(Gateway::class);
    $mailerDummy = $this->createMock(Mailer::class);

    $subscription = new Subscription($gatewaySpy, $mailerDummy);
    $subscription->create(new User('Maria'));

    $gatewaySpy->shouldHaveReceived('activate');
    $gatewaySpy->shouldNotHaveReceived('deactivate');
}
```

Logo no começo foi definido que não haveria nenhuma afirmação, já que não faz parte do objetivo do teste. E como não precisamos testar nada do fluxo de envio de email, voltamos a usar um Dummy para a classe `Mailer`.

O Spy que substituiu o gateway de pagamento verifica depois de todo o fluxo executado quais funções foram chamadas e quais não foram, validando que houve uma chamada para a função `activate` e nenhuma para a função `deactivate`.

## Test Doubles e Refatoração <a name="refatoracao"></a>

Usar um *test double* requer que saibamos de um certo nível de detalhe sobre a implementação do que estamos testando. 

Nos cenários que testamos aqui foi preciso saber, por exemplo, que para um usuário ser considerado assinante ele deveria salvar o UUID que retornou da integração com o meio de pagamento. O teste fica vinculado à implementação.

Quando refatoramos o código é possível que o teste também precise ser refatorado. Então cabe ao desenvolvedor saber quando é necessário usar o *test double* ou quando podemos deixar para que a classe resolva o que precisa ser resolvido usando objetos reais.

## Cuidado com o SUT <a name="cuidado-sut"></a>

Quando executamos os testes estamos validando algum aspécto do SUT.

Os *test doubles* estão ai para nos ajudar a lidar com dependências de um ou mais componentes, isolar o objeto de teste, simplificar comportamentos e nos dar mais controle sobre a situação. 

Às vezes se torna tentador criar um *test double* para substituir parte do SUT e essa é uma ideia ruim. 

Se você tiver vontade de substituir o comportamento de determinada função da classe que está testando, por exemplo, veja como um indicativo de que aquele código possui uma complexidade alta e talvez faça sentido ser encapsulada em uma classe própria.

----

Bom, esse post acabou ficando maior do que eu imaginei, então vou parar por aqui.

Caso tenha ficado alguma dúvida pode deixar nos comentários que eu respondo assim que possível. E vou deixar aqui também alguns recursos que me ajudaram enquanto montava esse post:

[Repositório com o código](https://github.com/thmarra/example-test-doubles)

[Test Double - Martin Fowler]([https://martinfowler.com/bliki/TestDouble.html](https://martinfowler.com/bliki/TestDouble.html))

[Test Doubles Swift - Matheus de Vasconcelos]([https://medium.com/concretebr/test-doubles-swift-cd43372eb45e](https://medium.com/concretebr/test-doubles-swift-cd43372eb45e))

[Test Doubles - Pablo Rodrigo Darde]([https://medium.com/rd-shipit/test-doubles-mocks-stubs-fakes-spies-e-dummies-a5cdafcd0daf](https://medium.com/rd-shipit/test-doubles-mocks-stubs-fakes-spies-e-dummies-a5cdafcd0daf))

[Don't Stub the System Under Test - Joe Ferris]([https://thoughtbot.com/blog/don-t-stub-the-system-under-test](https://thoughtbot.com/blog/don-t-stub-the-system-under-test))

[PHP Testing Jargon - Laracasts]([https://laracasts.com/series/php-testing-jargon](https://laracasts.com/series/php-testing-jargon))

Até a próxima!