Functional Testing with PHPUnit🔗
This cookbook will reference various practices and code samples about functional/end-to-end testing in a Symfony application and PHPUnit test suite.
Speedup your test suite by disabling debug mode 🔗
By default, functional tests booting the Symfoiny Kernel are using the debug mode. You can disable the debug mode in order to speedup your test suite.
In your phpunit.xml.dist
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
As the test container might not always be rebuilt on changes since debug mode is off,
ensure the cache is cleared with no debug in your make test
targets:
1 2 3 4 5 6 |
|
Debug your functional tests with the Symfony Profiler 🔗
Debugging functional tests might not always be easy, as most of the application is like a black box at this point and information about the error might not be in the response, or difficult to find/reason about from generated HTML response.
Hence, the easiest way to debug such tests is to directly access the application in a web context and inspect the Symfony Web Profiler.
In your ansible/group_vars/app.yml
, add the following host entry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
This will allow you to access the app in test env, with debug mode on.
But so far, the profiler isn't available. in order to enable it, change the following file:
1 2 3 4 5 6 7 8 |
|
and add profiler routes to your test env:
1 2 3 4 5 6 7 8 9 |
|
You should now be able to access the profiler in test env by opening http://test.your-app-host.vm/_profiler/
However, if you followed the "Speedup your test suite by disabling debug mode" section,
debug mode is off by default when your tests are executed, hence are not collected by the profiler.
Use APP_DEBUG=1 bin/phpunit --filter=YourTestClass::yourTestCaseMethod
to force execute your test in debug mode,
so you can inspect it in the web profiler.
Enable debug mode per test-case🔗
In some case, you might want to enable Symfony kernel debug mode for specific test cases (for instance, in order to access Profiler & collector data).
In order to do this, you can register the SymfonyDebugModeListener in your phpunit.xml.dist
:
1 2 3 4 5 6 7 8 9 10 |
|
Then, use the debug
PHPUnit group on your test case:
1 2 3 4 5 6 7 |
|
which should be enough for \Symfony\Bundle\FrameworkBundle\Test\KernelTestCase
test cases to create a kernel with debug mode on.
Writing tests with auto-updated expectations 🔗
Writing & updating functional tests should not be a tedious task for the developer.
That's why one of the practice we use in several projects in order to ease this is using expectations files
and auto-updated expectations with results.
An expectation file is a file with a dumped result from a test case.
Results refers here to various things generated by your application to compare, depending on the nature of the tested feature.
Imagine testing a JSON or Graphql API. Writing a functional test usually requires:
- writing fixtures for the test case
- writing a test case
- providing an input (the request payload)
- performing the request
- comparing the result, i.e: the generated JSON response
By simply using the ExpectationsTestTrait
trait in your test class,
you can use the ExpectationsTestTrait::assertStringMatchesExpectationsFile(string $result, string $expectationsFilePath)
method
to compare a result normalized as a string (here it's pretty obvious: the json response) with an expectation file stored in your project.
Nothing fancy, but the magic happens whenever you need to init or update these expectations files
(I.e: a new field is added to the JSON response): by running the test suite (or the single test case using bin/phpunit --filter=...
) with a specific flag, you can auto-update the expectations files with the test case results.
Usually, an env var is used for this purpose:
1 |
|
so it's easily actionable.
This requires a small setup for your test suite, in a tests/bootstrap.php
file:
1 2 3 4 5 6 7 8 9 10 11 |
|
If not already done, reference this file as the test suite bootstrapping file in your phpunit.xml.dist
file:
1 2 3 4 5 6 7 8 9 10 11 |
|
Warning
Whenever you're using the expectations auto-update feature, you MUST always carefully check the diff using
your preferred Git tool to ensure any change is actually expected.
You'll also get warnings inviting you to double-check updated expectations files that were previously containing
string formats (%s, %d, ...), as they were replaced by the actual data and will probably need partial revert.
Using a Git GUI interface like Tower or PhpStorm versionning tools makes this task pretty handy.
Going further🔗
The scenario mentioned here is pretty straightforward to understand: an API JSON response retrieved as string is compared to a file.
But this feature can be way more powerful coupled with the Symfony VarDumper component in order to be able compare anything (objects as well) and can really simplify writing test cases about anything produced by your application.
Some projects use this to collect and update expectations based on the command and query buses of your app, allowing to dump input (queries/commands) and their handlers results.
Some of those projects:
Libraries/projects using a similar approach🔗
- https://github.com/spatie/phpunit-snapshot-assertions uses the same approach to take "snapshots" automatically dumped into files to be compared in a test case.
Structure your test cases resources 🔗
Tests (especially functional ones) often require some resources:
- fixtures (specifically loaded for your test case)
- inputs (payload sent to an API, csv file read by a CLI command, ...)
- expectations (an API json response, an object dumped with the VarDumper component, a CSV file generated by a process, ...). See Writing tests with auto-updated expectations for more info.
In order to help you to manage these resources, you can use the TestCaseResourcesTrait
.
It relies on some conventions on the way you store these resources by:
- using the test class name as dir path where to store them
- using test cases & dataset names as file paths
This trait exposes numerous methods allowing you to get paths or content of these files.
Loading fixtures🔗
By using the FixturesTestTrait trait, you can benefit from the loadFixtures()
and appendFixtures()
methods from within your test cases.
This trait relies on two PHP packages:
- Nelmio Alice => Load fixture objects from files with a dedicated syntax
- Theofidry AliceDataFixtures => Persist loaded fixtures into the database using Doctrine
As well as a bridge service, which you can use to load fixtures in dev env too:
1 2 3 4 5 6 |
|
The trait requires you to provide a base dir from where to load your fixtures files, but this can be automatically provided by convention when relying on the TestCaseResourcesTrait.
You can load fixtures listed in a getFixtureFiles
method before each of your test cases with something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
GraphQL test cases 🔗
The GraphQLTestTrait and GraphQLTestCase base class
exposes methods to perform GraphQL requests and assertions based on test cases resources conventions (cf previous sections).
It loads a query/mutation from a .graphql file, performs the request with provided variables and files.
If the call is successful, the response can be compared with an expectation file (for instance using the JsonAssertionsTrait::assertJsonResponseMatchesExpectations()
method).
If the call is erroneous, the trait formats a bit the error and trace.
Login 🔗
The official documentation suggest two ways to login as a user in your tests cases:
Since Symfony 5.1, new methods will be available to ease this. Also see the related blog post about this method.
But until there, you may see in existing applications the two previous mentioned methods in use.
Using an HTTP basic auth🔗
The HttpBasicLoginTrait exposes a loginAs
method to set
credentials headers on a client, so you're authenticated when performing a request.
It requires enabling an HTTP basic auth on your firewall(s), at least in test env:
1 2 3 4 5 6 |
|
Forging a token in session🔗
The SessionLoginTrait exposes a method to forge a token to be stored in the session (the code used to fetch the user has to be adapted to your app).
Compose your tests 🔗
In this cookbook we expose you multiple traits you can use depending on your needs. By using traits, we add some flexibility and reusability for specifics needs. Each of these traits work on their own by defining contracts on their requirements. Not every app needs everything. Compose your base test classes accordingly.
The FunctionalTestTrait is a sample trait compound of other traits
and using a kernel. It can be used as is in a base test class or adapted to your app.
ControllerTestCase and GraphQLTestCase are base classes samples using this trait.