Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Customizing Browser Globally

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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:

... 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:

... 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:

... 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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice