Mocking Browsershot library in your tests
Tags: browsershot, testing, php, pest
Published: 2023-09-05 20:29:38

In our journey as developers, we often find ourselves creating applications that need to export reports or pages to PDF format. For a long time, we used various libraries for this task, such as **mPDF**, **FPDF**, **wkHtmlToPdf**, among others. However, today, in my humble opinion, we have one of the best packages for PDF generation on the market, which is [Browsershot](https://spatie.be/docs/browsershot/v2/introduction). It's straightforward to configure and generate PDF files.

But, here's the issue some devs face: How can I write tests for a class that uses Browsershot? Let's dive a bit deeper.

Imagine we have a class called GeneratePdf that takes a file name, a URL to render, and maybe the paper size as parameters. This class will save our PDF to AWS S3.

> ⚠️ The examples here are written in a Laravel application and using Pest for automated tests.

```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;
    }
}
```

Fantastic! Our action will save the PDF and return the path so that we can use it in an email, save it in a database, and so on. The only responsibility of this class is to generate the PDF and return the path.

But now, how do we test this little guy?

## Writing Our Tests

Okay, in this phase, let's write a simple test to see if everything works as expected.

```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);
});
```

However, you might notice that our test takes a while to execute. But why?

Our test took a while because Browsershot made a request to **google.com** to fetch its content and create the PDF for you.

Alright, it's just one test, what's the harm in that? Let's think:

- What if there's more than one class using Browsershot?
- What if you have no internet connection? **The test fails.**
- What if you're using a paid pipeline service? **The test will take longer, and you'll pay more for it.**

So, how can we write our test more efficiently?

with **_MOCKERY_** ✨✨✨

## Mockery

To simulate the behavior of a class, we can use the `Mockery` library, which is already available in PHPUnit and Pest.

This library provides an interface where we can mimic or spy on our class's behavior to make assertions on the methods that were called.

But there's a problem (there always is), a static call...
```php
BrowserShot::url(...)
```

### The Problem with Static Methods

Static methods are great, especially for helper classes, such as a method that checks whether a CPF (Brazilian social security number) is valid or not. In such cases, since we won't have access to `$this`, we can make these methods static without any issues.

However, this comes at a cost...

Writing unit tests for static methods is straightforward. We call the method and make the necessary assertions, simple as that. But what if I need to mock a class that calls a static method and then calls its non-static methods?

According to the Mockery [documentation](https://docs.mockery.io/en/latest/reference/public_static_properties.html), it doesn't support mocking public static methods. To work around this, there's a kind of hack to bypass this behavior, which involves creating an alias. (You can read more about it [here](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);
});
```

Alright, but what does this do? When we use `alias:`, we're telling Composer:

> "Hey, when I need Browsershot, bring this one here to me, not the original class."

The catch is that even Mockery doesn't recommend using `alias:` or `overload:`. This can lead to class name collision errors and should be run in separate PHP processes to avoid this.

> So, my friend, how do I write this test?

In fact, let's change the approach on how we use Browsershot :)

## Dependency Analysis and Dependency Injection
By analyzing the `Browsershot::url` method, we can discover what it does, and it's extremely simple.

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

Great, to avoid using `alias:` or `overload:`, we can simply inject Browsershot into our class. Now, it looks like this:

```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;
    }
}
```

This way, mocking becomes much lighter and efficient:

```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);
});
```

If you're using Laravel, you can use the `$this->mock` method, which interacts directly with the framework's container.

Our test now looks like this:

```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);
});
```

By doing this, we make our class loosely coupled, allowing us to perform a wide range of tests without much hassle, and we get to use a powerful pattern, which is dependency injection.

Until next time, folks. 😗 🧀