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. 😗 🧀
Top comments (0)