Mockando a biblioteca BrowserShot em seus testes
Tags: browsershot, testing, pest, laravel
Published: 2023-09-05 12:08:40

Em nossa carreira como dev acabamos por fazer várias aplicações que necessitam exportar algum relatório, ou página, em PDF. Por muito tempo, nós usamos várias bibliotecas para isso, como a **mPDF**, **FPDF**, **wkHtmlToPdf** dentre outras. Hoje temos, na minha humilde opinião, um dos melhores packages para geração de PDF no mercado, que é o [Browsershot](https://spatie.be/docs/browsershot/v2/introduction). Muito simples de configurar e gerar arquivos PDF pra gente.

Porém, vejo alguns devs com o seguinte problema: Como posso escrever testes para uma classe que vai fazer uso do Browsershot? Vamos mergulhar um pouco mais.

Vamos imaginar que temos uma classe chamada **GeneratePdf** e que aceitará um nome para o arquivo, uma URL para ser renderizada e, talvez, o tamanho do papel. Essa classe irá salvar o nosso PDF na AWS S3.

> ⚠️ Os exemplos aqui escritos foram feitos em uma aplicação Laravel e utilizando o pest para testes automatizados

```php
<?php

declare(strict_types=1);

namespace App\Actions;

use Illuminate\Support\Facades\Storage;
use Spatie\Browsershot\Browsershot;

class GeneratePdf
{
    public function handle(
        string $fileName,
        string $url,
        string $paperSize = 'A4'
    ): string | false {
        $path    = '/exports/pdf/' . $fileName;
        $content = Browsershot::url($url)
            ->format($paperSize)
            ->noSandbox()
            ->pdf();

        if (!Storage::disk('s3')->put($path, $content)) {
            return false;
        }

        return $path;
    }
}
```

Maravilha, a nossa `action` vai salvar o PDF e retornar o caminho para que possamos utilizar ele em um e-mail, salvar em um banco de dados, etc. A única responsabilidade dessa classe é gerar o PDF e retornar o caminho.

Mas, e agora, como faço para testar esse carinha?

## Escrevendo meus testes

Beleza, nessa fase escrevemos um teste simples para ver se tudo vai funcionar como esperamos.

```php
it('should generate a pdf', function () {
    Storage::fake('s3');

    $pdf = (new GeneratePdf())->handle(
        fileName: 'my-file-name.pdf',
        url: 'https://www.google.com'
    );

    Storage::disk('s3')
        ->assertExists($pdf);
});
```

Contudo, podemos notar que o nosso teste vai demorar um pouco. Mas, por quê?

Nosso teste demorou a ser executado por que o Browsershot fez uma requisição ao **google.com** para pegar seu conteúdo e montar o pdf pra você.

Ok, ok. É um teste apenas, que mal há nisso? Vamos pensar:

* E se houver mais de uma classe que faz uso do Browsershot?
* E se você estiver sem internet? - **O teste falha**;
* E se você estiver utilizando um serviço de pipeline pago? **O teste vai demorar e você vai pagar a mais por isso**;

Então Matheusão, como faço para escrever meu teste de uma forma mais eficiente?

Com **_MOCKERY_** ✨✨✨

## Mockery
Para que possamos simular o comportamento de uma classe, podemos usar a biblioteca `Mockery`, que já vem disponível no PHPUnit e no Pest.

Essa lib provê uma interface onde eu posso simular o comportamento da minha classe, ou expiá-la, para que possamos  fazer o assert dos métodos que foram chamados.

Mas existe um problema (sempre ele), uma chamada estática...

```php
BrowserShot::url(...)
```

### O problema dos métodos estáticos.

Métodos estáticos são legais, principalmente para classes de helpers, como por exemplo, um método que checa se um CPF é válido ou não.

Nesses casos, como sei que não terei acesso ao `$this`, posso  desenhar esse método para ser estático, sem problema algum.

Porém, isso tem um custo...

Fazer testes unitários para métodos mágicos é muito simples. Chamo o meu método e faço as asserções que preciso, simples assim. Mas e quando eu preciso mockar uma classe que está chamando um método estático e, logo após, chama os métodos não estáticos dela?

Segundo a [documentação](https://docs.mockery.io/en/latest/reference/public_static_properties.html) do mockery, ele não suporta o mocking de métodos públicos estáticos. Para fazer isso, existe uma espécie de `hack` para burlar esse comportamento, que é criando um alias. (Você pode ler mais [aqui](https://docs.mockery.io/en/latest/reference/public_static_properties.html#:~:text=See%20the-,Aliasing,-section%20for%20more))

```php
it('should generate a pdf', function () {
    Storage::fake('s3');

    mock('alias:' . Browsershot::class)
        ->shouldReceive('url->format->noSandbox->pdf');

    $pdf = (new GeneratePdf())->handle(
        fileName: 'my-file-name.pdf',
        url: 'https://www.google.com'
    );

    Storage::disk('s3')
        ->assertExists($pdf);
});
```

Show, mas, o que isso faz? Quando usamos o `alias:`, nós dizemos ao composer:

> Olha, quando eu precisar do Browsershot, traz esse carinha aqui pra mim, não a classe original.

O detalhe é que nem a própria mockery recomenda que façamos uso do `alias:` ou do `overload:`. Isso pode causar erros de colisão de nomes de classes e devem ser executados em processos PHP separados para evitar isso. 

> Pô amigo, como vou escrever esse teste?

Na verdade, vamos mudar a abordagem de como usamos o Browsershot :)

## Análise de dependência e Dependency Injection

Ao analisar o método `Browsershot::url`, podemos descobrir o que ele faz, e é extremamente simples.

```php
public static function url(string $url): static
{
    return (new static())->setUrl($url);
}
```

Massa, então para evitar o uso de `alias:` ou `overload:`, podemos simplesmente injetar o browsershot em nossa classe. Agora ela fica assim:

```php
<?php

declare(strict_types=1);

namespace App\Actions;

use Illuminate\Support\Facades\Storage;
use Spatie\Browsershot\Browsershot;

class GeneratePdf
{
    public function __construct(
        private Browsershot $browsershot
    ) {
    }

    public function handle(
        string $fileName,
        string $url,
        string $paperSize = 'A4'
    ): string | false {
        $path    = '/exports/pdf/' . $fileName;
        $content = $this->browsershot->setUrl($url)
            ->format($paperSize)
            ->noSandbox()
            ->pdf();

        if (!Storage::disk('s3')->put($path, $content)) {
            return false;
        }

        return $path;
    }
}

```

Dessa forma, o mock fica muito mais leve e eficiente:

```php
it('should generate a pdf', function () {
    Storage::fake('s3');

    $mock = mock(Browsershot::class);
    $mock->shouldReceive('setUrl->format->noSandbox->save');

    $pdf = (new GeneratePdf($mock))->handle(
        fileName: 'my-file-name.pdf',
        url: 'https://www.google.com'
    );

    Storage::disk('s3')
        ->assertExists($pdf);
});
```

Se você estiver utilizando o Laravel, pode usar o método `$this->mock` que vai interagir diretamente com o container do framework.

Nosso teste fica assim:

```php
it('should generate a pdf', function () {
    Storage::fake('s3');
    Storage::put('pdf/my-file-name.pdf', 'my-fake-file-content');
    
    $this->mock(Browsershot::class)
        ->shouldReceive('setUrl->format->noSandbox->save');

    $pdf = app(GeneratePdf::class)->handle(
        fileName: 'my-file-name.pdf',
        url: 'https://www.google.com'
    );

    Storage::disk('s3')
        ->assertExists($pdf);
});
```

Dessa forma, deixamos a nossa classe fracamente acoplada, com uma ampla gama de testes que podemos fazer sem penar muito e, de quebra, ainda podemos fazer uso de um pattern poderoso que é a injeção de dependência.

Até a próxima, pessoal. 😗 🧀