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 SubscribeLet's create a test to post and create a new treasure. Say public function testPostToCreateTreasure()
that returns void
. And start the same way as before: $this->browser()->post('/api/treasures')
:
... lines 1 - 10 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 13 - 40 | |
public function testPostToCreateTreasure(): void | |
{ | |
$this->browser() | |
->post('/api/treasures', [ | |
... line 45 | |
]) | |
... lines 47 - 48 | |
; | |
} | |
} |
In this case we need to send data. The second argument to any of these post()
or get()
methods is an array of options, which can include headers
, query
parameters or other stuff. One key is json
, which you can set to an array, which will be JSON-encoded for you. Start by sending empty JSON... then ->assertStatus(422)
. To see what the response looks like, add ->dump()
:
... lines 1 - 10 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 13 - 40 | |
public function testPostToCreateTreasure(): void | |
{ | |
$this->browser() | |
->post('/api/treasures', [ | |
'json' => [], | |
]) | |
->assertStatus(422) | |
->dump() | |
; | |
} | |
} |
Awesome! Copy the test method name. I want to focus just on this one test. To do that, run:
symfony php bin/phpunit --filter=testPostToCreateTreasure
And... oh! Current response status code is 401, but 422 expected.
When a test fails with browser, it automatically saves the last response to a file... which is awesome. It's actually in the var/
directory. In my terminal, I can hold Command
and click to open that in my browser. That is nice. You'll see me do this a bunch of times.
Ok, so this returned a 401 status code. Of course: the endpoint requires authentication! Our app has two ways to authenticate: via the login form and session or via an API token. We're going to test both, starting with the login form.
To log in as a user... that user first needs to exist in the database. Remember: at the start of each test, our database is empty. It's then our job to populate it with whatever we need.
Create a user with UserFactory::createOne(['password' => 'pass'])
so that we know what the password will be. Then, before we make the POST request to create a treasure, ->post()
to /login
and send json
with email
set to $user->getEmail()
- to use whatever random email address Faker chose - then password
set to pass
. To make sure that worked, ->assertStatus(204)
:
... lines 1 - 5 | |
use App\Factory\UserFactory; | |
... lines 7 - 11 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 14 - 41 | |
public function testPostToCreateTreasure(): void | |
{ | |
$user = UserFactory::createOne(['password' => 'pass']); | |
$this->browser() | |
->post('/login', [ | |
'json' => [ | |
'email' => $user->getEmail(), | |
'password' => 'pass', | |
], | |
]) | |
->assertStatus(204) | |
... lines 54 - 58 | |
; | |
} | |
} |
That's the status code we're returning after successful authentication.
Let's give this a try! Move over and run the test:
symfony php bin/phpunit --filter=testPostToCreateTreasure
It passes! We're getting the 422 status code and see the validation messages!
So... logging in is... just that easy! And I would recommend having a test that specifically POSTs to your login endpoint like we just did, to make sure its working correctly.
However, in all of my other tests... when I simply need to be authenticated to do the real work, there's a faster way to log in. Instead of making the POST request, say ->actingAs($user)
:
... lines 1 - 11 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 14 - 41 | |
public function testPostToCreateTreasure(): void | |
{ | |
... lines 44 - 45 | |
$this->browser() | |
->actingAs($user) | |
... lines 48 - 52 | |
; | |
} | |
} |
This is a sneaky way of taking the User
object and pushing it directly into Symfony's security system without making any requests. It's easier, and faster. And now, we don't care what the password is at all, so we can simplify that.
Let's check it:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Still good!
Let's do another POST
down here. Keep chaining and add ->post()
. Actually... I'm lazy. Copy the existing ->post()
... and use that. But this time, send real data: I'll quickly type in some... these can be anything. The last key we need is owner
. Right now, we are required to send the owner
when we create a treasure. Soon, we'll make that optional: if we don't send it, it will default to whoever is authenticated. But for now, set it to /api/users/
then $user->getId()
. Finish with assertStatus(201)
:
... lines 1 - 11 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 14 - 41 | |
public function testPostToCreateTreasure(): void | |
{ | |
$user = UserFactory::createOne(); | |
$this->browser() | |
->actingAs($user) | |
->post('/api/treasures', [ | |
'json' => [], | |
]) | |
->assertStatus(422) | |
->post('/api/treasures', [ | |
'json' => [ | |
'name' => 'A shiny thing', | |
'description' => 'It sparkles when I wave it in the air.', | |
'value' => 1000, | |
'coolFactor' => 5, | |
'owner' => '/api/users/'.$user->getId(), | |
], | |
]) | |
->assertStatus(201) | |
; | |
} | |
} |
Because 201 is what the API returns when an object is created.
Alright, go test, go:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Still passing! We're on a roll! Add a ->dump()
to help us debug then a sanity check: ->assertJsonMatches()
that name
is A shiny thing
:
... lines 1 - 11 | |
class DragonTreasureResourceTest extends KernelTestCase | |
{ | |
... lines 14 - 41 | |
public function testPostToCreateTreasure(): void | |
{ | |
... lines 44 - 45 | |
$this->browser() | |
... lines 47 - 60 | |
->assertStatus(201) | |
->dump() | |
->assertJsonMatches('name', 'A shiny thing') | |
; | |
} | |
} |
When we try that:
symfony php bin/phpunit --filter=testPostToCreateTreasure
No surprise: all green. But look at the dumped response: it's not JSON-LD! We're getting back standard JSON. You can see it in the Content-Type
header: 'application/json'
, not application/ld+json
, which is what I was expecting.
Let's find out what's going on next and fix it globally by customizing how Browser works across our entire test suite.
"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
}
}