Lucky you! You found an early release chapter - it will be fully polished and published shortly!
Rest assured, the gnomes are hard at work
completing this video!
Our API is getting more and more complex. And doing manually testing is not a great long-term plan. So let's install some tools to get a killer test setup.
Step one: at your terminal run:
composer require test
This is a flex alias for a package called symfony/test-pack
. Remember: packs are
shortcut packages that actually install a bunch of other packages. For example,
when this finishes... and we check out composer.json
, you can see down in
require-dev
that this added PHPUnit itself as well as a few other tools from
Symfony to help testing.
It also executed a recipe which added a number of files. We have phpunit.xml.dist
,
a tests/
directory, .env.test
for test-specific environment variables and
even a little bin/phpunit
executable shortcut that we'll use to run our tests.
No surprise, Symfony has tools for testing and these can be used to test an API. Heck, API platform even has their own tools built on top of those to make testing an API even easier. And yet, I'm going to be stubborn and use a totally different tool that I've fallen in love with.
It's called Browser, and it's also built on top of Symfony's testing tools: almost like a nicer interface above that strong base. It's just... super fun to use. Browser gives us a fluid interface that can be used for testing web apps, like you see here, or testing APIs. It can also can be used to test pages that use JavaScript.
Let's get this guy install. Copy the composer require
line, spin back over and
run that:
composer require zenstruck/browser --dev
While that's doing its thing, it's optional, but there's an "extension" that you
can add to phpunit.xml.dist
. Add it down here on the bottom. In the future,
if you're using PHPUnit 10, this will likely be replaced by some listener
config.
This adds a few extra features to browser. Like, when a test fails, it will automatically save the last response to a file. We'll see this soon. And if you're using JavaScript testing, it'll take screenshots of failures!
Ok, we're ready for our first test. In the tests/
directory, it doesn't
matter how you organize things, but I'm going to create a Functional/
directory because we're going to be making functional tests to our API. Yup,
we'll literally create an API client, make GET or POST requests and then assert
that we get back the correct output.
Create a new class called DragonTreasureResourceTest
. A normal test extends
TestCase
from PHPUnit. But make this extend KernelTestCase
: a class from
Symfony that extends TestCase
... but gives us access to Symfony's engine.
Let's start by testing the GET collection endpoint to make sure we get back
the data we expect. To activate the browser library, at the top, add a trait
with use HasBrowser
.
Next, add a new test method: public function
, how about
testGetCollectionOfTreasures()
... which will return void
. Using browser is dead
simple thanks to that trait: $this->browser()
. Now we can make GET, POST, PATCH
or whatever request we want. Make a GET request to /api/treasures
and then,
just to see what that looks like, use this nifty ->dump()
function.
How cool is that? Let's see what it looks like. To execute our test, we could run:
./vendor/bin/phpunit
That works just fine. But one of the recipes also added a shortcut file:
php bin/phpunit
When we run that, ooh, let's see. The dump()
did happen: it dumped out the
response... which was some sort of error. It says:
SQLSTATE: connection to server port 5432 failed.
Hmm, it can't connect to our database. Our database is running via a Docker
container... and then, because we're using the symfony
web server, when we use
the site via a browser, the symfony
web server detects the Docker container and
sets the DATABASE_URL
environment variable for us. That's how our API has been
able to talk to the Docker database.
When we've run commands that need to talk to the database, we've been running
them like symfony console make:migration
... because when we execute things through
symfony
, it adds the DATABASE_URL
environment variable... and then runs the
command.
So, when we simply run php bin/phpunit
... the real DATABASE_URL
is missing.
To fix that, run:
symfony php bin/phpunit
It's the same thing... except it lets symfony
add the DATABASE_URL
environment variable. And now... we see the dump again! Scroll to the top. Better!
Now the error says:
Database
app_test
does not exist.
Interesting. To understand what's happening, open config/packages/doctrine.yaml
.
Scroll down to a when@test
section. This is cool: when we're in the test
environment, there's a bit of config called dbname_suffix
. Thanks to this, Doctrine
will take our normal database name and add _test
to it.
This next part is specific to a library called ParaTest where you can run tests
in parallel. Since we're not using that, it's just an empty string and not something
we need to worry about.
Anyway, that's how we end up with an _test
at the end of our database name. And
we want that! We don't want our dev
and test
environments to use the same database
because it gets annoying when they run over each other's data.
By the way, if you're not using the symfony
Binary and Docker
setup... and you're configuring your database manually, be aware that in the test
environment, the .env.local
file is not read. The test
environment
is special: it skips reading .env.local
and only reads .env.test
.
You can also create a .env.local.test
for env vars that are read in the
test
environment but that won't be committed to your repository.
Ok, in the test
environment, we're missing the database. We could easily fix this
by running:
symfony console doctrine:database:create --env=test
But that's way too much work. Instead, add one more trait to our test class:
use ResetDatabase
.
This comes from Foundry: the library we've been using to create dummy fixtures
via the factory classes. ResetDatabase
is amazing. It automatically makes sure
that the database is cleared before each test. So if you have two tests, your
second test isn't going to mess up because of some data that the first test added.
It's also going to create the database automatically for us. Check it out. Run
symfony bin/phpunit
again and check out the dump. That's our response! It's our beautiful JSON-LD! We don't have any items in the collection yet, but it is working.
And notice that, when we make this request, we are not sending an Accept
header on the request. Remember, when we use the Swagger UI... it actually does
send an Accept
header that advertises that we want application/ld+json
.
We can add that to our test if we want. But if we pass nothing, we get JSON-LD back because that's the default format of our API.
Next: let's properly finish this test, including seeding the database with data and learning about Browser's API assertions.
"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
}
}