Mocking Browsershot for faster tests

March 2nd, 2022

Spatie's Browsershot is a wonderful package that uses puppeteer to render a web page to transform it into an image or PDF. Having this functionality covered by tests generally requires you to install puppeteer in your CI environment and can slow down your tests a lot.

In this post I'll show you how you can mock the two important Browsershot methods so you can still assert that Browsershot is called in your code, without having to install & run puppeteer.

One caveat: if you're testing the actual contents of the image or PDF that Browsershot generates, you won't be covered anymore, I suggest testing the contents of the view that Browsershot will load in production to make sure everything you need is shown and trust that Browsershot will generate it correctly (it is covered by tests in the package itself).

Setting up our test case

First of all we'll set up our simple test case, we'll have a controller that calls Browsershot and returns the generated PDF to the browser. What's important to note here is that we inject the Browsershot instance into the controller. We're also using the Temporary Directory package to make it easier on us to serve the PDF.

use Spatie\Browsershot\Browsershot;
use Spatie\TemporaryDirectory\TemporaryDirectory;

namespace App\Controllers;

class DownloadPDFController
{
    public function __invoke(Browsershot $browsershot) {
        $temporaryDirectory = (new TemporaryDirectory())->create();

        $browsershot
          ->url('https://example.com')
          ->landscape()
          ->save($temporaryDirectory->path('download.pdf'));

        return response()->file($temporaryDirectory->path('download.pdf'));
    }
}

Once this is set up, we'll create a test to verify that our controller is returning a PDF file to us.

use Tests\TestCase;
use App\Controllers\DownloadPDFController;

class DownloadPDFTest extends TestCase
{
    /** @test * */
    public function it_can_download_a_pdf(): void
    {
        $response = $this->get(action(DownloadPDFController::class));

        $response
            ->assertSuccessful()
            ->assertHeader('content-type', 'application/pdf');
    }
}

This basic ->assertHeader('content-type', 'application/pdf'); assertion tests that what we receive is a PDF file, however it still goes through the full Browsershot with Puppeteer workflow now, and as a result this test is pretty slow.

Mocking Browsershot

Laravel makes it easy for us to create a mock of any class by using $this->mock() inside our tests, however the problem with a full mock is that you need to specify each individual method that will be called on the mock, in this case we would have to mock the ->url and ->landscape methods as well, even though we want these to work as they normally would.

A partial mock instead allows us to only override the methods that we want to mock, in this case the ->save() method, which is where Browsershot calls out to Puppeteer to render the page, we can set this up like this:

$this->partialMock(Browsershot::class)
  ->shouldReceive('save')
  ->andReturnUsing(function ($path): void {
      file_put_contents($path, base64_decode("JVBERi0xLg10cmFpbGVyPDwvUm9vdDw8L1BhZ2VzPDwvS2lkc1s8PC9NZWRpYUJveFswIDAgMyAzXT4+XT4+Pj4+Pg=="));
  });

The base64 encoded string is the smallest possible valid PDF. Which is perfect for our use case where we want to ensure Browsershot is called and we get a PDF download.

Laravel will then replace the Browsershot instance in the container with our partial mock, all other methods will just pass through to the actual implementation, until we call the ->save method and our mock returns the small PDF.

If you're using Browsershot to generate both PDF and PNG files, I suggest mocking the ->save() method with a small PNG image and using savePdf() for all your PDF generating and mocking that one with the PDF file.

MENU