Buy Access to Course
18.

Customizing Browser Globally

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our test works... but the API is sending us back JSON, not JSON-LD. Why?

When we made the GET request earlier, we did not include an Accept header to indicate which format we wanted back. But... JSON-LD is our API's default format, so it sent that back.

However, when we make a ->post() request with the json key, that adds a Content-Type header set to application/json - which is fine - but it also adds an Accept header set to application/json. Yup, we're telling the server that we want plain JSON back, not JSON-LD.

I want to use JSON-LD everywhere. How can we do that? The second argument to ->post() can be an array or an object called HttpOptions. Say HttpOptions::json()... and then pass the array directly. Let me... get my syntax right:

// ... lines 1 - 7
use Zenstruck\Browser\HttpOptions;
// ... lines 9 - 12
class DragonTreasureResourceTest extends KernelTestCase
{
// ... lines 15 - 42
public function testPostToCreateTreasure(): void
{
// ... lines 45 - 52
->post('/api/treasures', HttpOptions::json([
'name' => 'A shiny thing',
'description' => 'It sparkles when I wave it in the air.',
'value' => 1000,
'coolFactor' => 5,
'owner' => '/api/users/'.$user->getId(),
]))
// ... lines 60 - 62
;
}
}

So far, this is equivalent to what we had before. But now we can change some options by saying ->withHeader() passing Accept and application/ld+json:

// ... lines 1 - 12
class DragonTreasureResourceTest extends KernelTestCase
{
// ... lines 15 - 42
public function testPostToCreateTreasure(): void
{
// ... lines 45 - 52
->post('/api/treasures', HttpOptions::json([
'name' => 'A shiny thing',
'description' => 'It sparkles when I wave it in the air.',
'value' => 1000,
'coolFactor' => 5,
'owner' => '/api/users/'.$user->getId(),
])->withHeader('Accept', 'application/ld+json'))
// ... lines 60 - 62
;
}
}

We could have also done this with the array of options: it has a key called headers. But the object is kind of nice.

Let's make sure this fixes things. Run the test:

symfony php bin/phpunit --filter=testPostToCreateTreasure

Globally Sending the Header

And... we're back to JSON-LD! It's got the right fields and the application/ld+json response Content-Type header.

So.... that's cool... but doing this every time we make a request to our API in the tests is... mega lame. We need this to happen automatically.

A nice way to do that is to leverage a base test class. Inside of tests/, actually inside of tests/Functional/, create a new PHP class called ApiTestCase. I'm going to make this abstract and extend KernelTestCase:

27 lines | tests/Functional/ApiTestCase.php
// ... lines 1 - 2
namespace App\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
// ... lines 6 - 9
abstract class ApiTestCase extends KernelTestCase
{
// ... lines 12 - 25
}

Inside, add the HasBrowser trait. But we're going to do something sneaky: we're going to import the browser() method but call it baseKernelBrowser:

27 lines | tests/Functional/ApiTestCase.php
// ... lines 1 - 7
use Zenstruck\Browser\Test\HasBrowser;
abstract class ApiTestCase extends KernelTestCase
{
use HasBrowser {
browser as baseKernelBrowser;
}
// ... lines 15 - 25
}

Why the heck are we doing that? Re-implement the browser() method... then call $this->baseKernelBrowser() passing it $options and $server. But now call another method: ->setDefaultHttpOptions(). Pass this HttpOptions::create() then ->withHeader(), Accept, application/ld+json:

27 lines | tests/Functional/ApiTestCase.php
// ... lines 1 - 5
use Zenstruck\Browser\HttpOptions;
// ... lines 7 - 9
abstract class ApiTestCase extends KernelTestCase
{
// ... lines 12 - 15
protected function browser(array $options = [], array $server = [])
{
return $this->baseKernelBrowser($options, $server)
->setDefaultHttpOptions(
HttpOptions::create()
->withHeader('Accept', 'application/ld+json')
)
;
}
}

Done! Back in our real test class, extend ApiTestCase: get the one that's from our app:

// ... lines 1 - 11
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 14 - 63
}

That's it! When we say $this->browser(), it now calls our browser() method, which changes that default option. Celebrate by removing withHeader()... and you could revert back to the array of options with a json key if you want.

Let's try it.

symfony php bin/phpunit --filter=testPostToCreateTreasure

And... uh oh. That's a strange error:

Cannot override final method _resetBrowserClients()

This... is because we're importing the trait from the parent class and our class... which makes the trait go bananas. Remove the one inside our test class:

// ... lines 1 - 8
use Zenstruck\Browser\Test\HasBrowser;
// ... lines 10 - 11
class DragonTreasureResourceTest extends ApiTestCase
{
use HasBrowser;
// ... lines 15 - 63
}

we don't need it anymore. I'll also do a little cleanup on my use statements.

And now:

symfony php bin/phpunit --filter=testPostToCreateTreasure

Got it! We get back JSON-LD with zero extra work. Remove that dump():

// ... lines 1 - 11
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 14 - 41
public function testPostToCreateTreasure(): void
{
// ... lines 44 - 45
$this->browser()
// ... lines 47 - 59
->dump()
// ... line 61
;
}
}

Next: let's write another test that uses our API token authentication.