If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur 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
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}