Mockando a biblioteca BrowserShot em seus testes
Matheus Lopes Santos
Posted on September 5, 2023
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. 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
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.
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...
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 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)
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.
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
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:
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:
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. 😗 🧀
Posted on September 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.