Since the very first version of Symfony 2, the framework provides a suite of convenient tools to create functional tests. They use the BrowserKit and DomCrawler components to simulate a web browser with a developer-friendly API.

All green! Symfony provides a very convenient API to navigate the website, check that links work and assert that the expected content is displayed. It's easy to setup, and it's super fast!

And add assertions to check that our controller works properly:

Thanks to the WebTestCase helper, adding some functional tests for this website is easy. First, generate a functional test skeleton:

This implementation isn't very dynamic, but it does the job. Then, we need a controller and the corresponding Twig template to display the latest news of the community. We'll use the Maker Bundle to generate them:

We're ready to code. Start by adding a class to store and retrieve the news:

Let's refresh our memories by creating a tiny news website, and the corresponding functional test suite:

There is also an experimental branch that uses Geckodriver to automatically start and drive a local installation of Firefox instead of Chrome.

Even if Chrome is the default choice, Panther can control any browser supporting the WebDriver protocol. It also supports remote browser testing services such as Selenium Grid (open source), SauceLabs and Browserstack .

Because both tools implement the same API, Panther can also execute web scraping scenarios written for the popular Goutte library . In test cases, Panther lets you choose if the scenario must be executed using the Symfony kernel (when available, static::createClient() ), using Goutte (send real HTTP queries but no JavaScript and CSS support, static::createGoutteClient() ) or using real web browsers ( static::createPantherClient() ).

As you may have noticed in the recording, I've added some calls to sleep() to highlight how it works. Having access to the browser's window (and to the Dev Tools) is also very useful to debug a failing scenario.

If you only believe what you see, try running the following:

All green, again! But this time, we're sure that our news website works properly in Google Chrome.

What's even better, to use Panther, you only need a local Chrome installation. There is nothing more to install: no Selenium (but Panther supports it too), no obscure browser driver or extension... Actually, because Panther is now a dependency of the symfony/test-pack metapackage, you've already installed Panther without knowing it when you've typed composer req --dev tests earlier. You could also install Panther directly in any PHP project by running composer require symfony/panther .

Well, Panther allows to run this exact same scenario in real browsers! It also implements the BrowserKit and DomCrawler APIs, but under the hood it uses the Facebook PHP WebDriver library. It means that you can choose to execute the same browsing scenario in a lightning-fast pure PHP implementation ( WebTestCase ) or in any modern web browser, through the WebDriver browser automation protocol which became an official W3C recommendation in June.

However, WebTestCase doesn't use a real web browser. It simulates one with pure PHP components. It doesn't even use the HTTP protocol: it creates instances of HttpFoundation's Request objects, pass them to the Symfony kernel, and allows to assert on the HttpFoundation Response instance returned by the app. Now, what if a problem preventing the webpage to load occurs in the browser? Such issues can be as diverse as a link hidden by a faulty CSS rule, a default form behavior prevented by a buggy JavaScript file , or, worst, the detection by the browser of a security vulnerability in your code .

Testing Client-side Generated HTML¶

Our news website looks good, and we've just proved that it works in Chrome. But now, we want to hear some feedback from the community about our frequent publications. Let's add a comment system to our website.

To do so, we'll leverage the capabilities of Symfony 4 and of the modern web platform: we'll manage the comments through a web API, and we'll render them using Web Components and Vue.js. Using JavaScript for this feature allows to improve the overall performance and user experience: each time we post a new comment, it will be displayed in the existing page without requiring a full reload.

Symfony provides an official integration with API Platform, probably the easiest way out there to create modern web APIs (hypermedia and/or GraphQL). Install it:

1 $ composer require api

Then, use the Maker Bundle again to create a Comment entity class, and expose it through a read and write API endpoint:

1 $ ./bin/console make:entity --api-resource Comment

This command is interactive, and allows to specify the fields to create. We need only two: news (the slug of the news) and body (the comment's content). news is of type string (maximum length of 255 chars), while body is of type text . Both aren't nullable.

Here is the full transcript of the interactions with the command:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 New property name (press <return> to stop adding fields): > news Field type (enter ? to see all types) [string]: > Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Comment.php Add another property? Enter the property name (or press <return> to stop adding fields): > body Field type (enter ? to see all types) [string]: > text Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Comment.php

Update the .env file to set the value of DATABASE_URL to the address of your RDBMS and run the following command to create the table corresponding to our entity:

1 $ ./bin/console doctrine:schema:create

If you open http://localhost:8000/api , you can see that the API is already working and documented 😁.

We'll make some minor modification to the generated Comment class. Currently, the API allows GET , POST , PUT and DELETE operations. This is too open. As we don't have any authentication mechanism for now, we only want our users to be able to create and read comments:

1 2 3 4 5 6 /** - * @ApiResource() + * @ApiResource( + * collectionOperations={"post", "get"}, + * itemOperations={"get"} + * )

Then, we want to be able to retrieve the comments posted on a specific news article. We'll use a filter in order to do that:

1 2 3 4 5 6 7 8 9 + use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiResource; + use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; /** * @ORM\Column(type="string", length=255) + * @ApiFilter(SearchFilter::class) */ private $news;

Finally, add some validation constraints to be sure the submitted comments are OK:

1 2 3 4 5 6 7 8 9 10 11 12 /** * @ORM\Column(type="string", length=255) + * @Assert\Choice(choices={"week-601", "symfony-live-usa-2018"}) * @ApiFilter(SearchFilter::class) */ private $news; /** * @ORM\Column(type="text") + * @Assert\NotBlank() */ private $body;

Reload http://localhost:8000/api , the changes are automatically taken into account.

Creating a custom validation constraint instead of hardcoding the list of available slugs in the choice assertion is left as an exercise to the reader.

That's all for the PHP part! Easy, isn't it? Let's consume our API with Vue.js! To do so, we'll use the Vue.js integration provided by Symfony Webpack Encore.

Install Encore and its Vue.js integration:

1 2 3 4 $ composer require encore # If you don't have the Yarn package manager yet, install it from https://yarnpkg.com/en/ $ yarn install $ yarn add --dev vue [email protected]^14 vue-template-compiler

Update Encore's config to enable the Vue loader:

1 2 3 4 5 6 // webpack.config.js Encore // ... + .addEntry('js/comments', './assets/comments/index.js') + .enableVueLoader()

We're ready to create some cool frontend! Let's start with a Vue component rendering the list of comments and a form to post a new one:

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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <!-- assets/comments/CommentSystem.vue --> <template> <div> <ol reversed v-if="comments.length"> <li v-for="comment in comments" :key="comment['@id']">{{ comment.body }}</li> </ol> <p v-else>No comments yet 🙁</p> <form id="post-comment" @submit.prevent="onSubmit"> <textarea name="new-comment" v-model="newComment" placeholder="Your opinion matters! Send us your comment."></textarea> <input type="submit" :disabled="!newComment"> </form> </div> </template> <script> export default { props: { news: {type: String, required: true} }, methods: { fetchComments() { fetch(`/api/comments?news=${encodeURIComponent(this.news)}`) .then((response) => response.json()) .then((data) => this.comments = data['hydra:member']) ; }, onSubmit() { fetch('/api/comments', { method: 'POST', headers: { 'Accept': 'application/ld+json', 'Content-Type': 'application/ld+json' }, body: JSON.stringify({news: this.news, body: this.newComment}) }) .then(({ok, statusText}) => { if (!ok) { alert(statusText); return; } this.newComment = ''; this.fetchComments(); }) ; } }, data() { return { comments: [], newComment: '', }; }, created() { this.fetchComments(); } } </script>

It wasn't that hard, was it?

Then, create the entrypoint for our comment app:

1 2 3 4 5 6 7 8 // assets/comments/index.js import Vue from 'vue' ; import CommentSystem from './CommentSystem' ; new Vue ({ el : '#comments' , components : { CommentSystem } });

Finally, reference the JavaScript file and initialize the <comment-system> web component with the current slug in the item.html.twig template:

1 2 3 4 5 6 7 8 9 10 11 12 13 {% block body %} <h1>{{ item.title }}</h1> {{ item.body }} + <div id="comments"> + <comment-system news="{{ item.slug }}"></comment-system> + </div> {% endblock %} + {% block javascripts %} + <script src="{{ asset('build/js/comments.js') }}"></script> + {% endblock %}

Build the transpiled and minified JS file (you may want to use Hot Module Reloading in dev):

1 $ yarn encore production

Wow! Thanks to Symfony 4, we have created a web API and a rich Vue.js webapp in just a few lines of code. Ok, let's add some tests for our comment system!

Wait... The comments are fetched using AJAX, and rendered client-side, in JavaScript. And the new comments are also added asynchronously using JS. Unfortunately, it will not be possible to use WebTestCase nor Goutte to test our new feature: they are written in PHP, and don't support JavaScript or AJAX 😱.

Don't worry, Panther is able to test such applications. Remember: under the hood it uses a real web browser!

Let's test our comment system:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace App\Tests ; use Symfony\Component\Panther\PantherTestCase ; class CommentsTest extends PantherTestCase { public function testComments () { $client = static :: createPantherClient (); $crawler = $client -> request ( 'GET' , '/news/symfony-live-usa-2018' ); $client -> waitFor ( '#post-comment' ); // Wait for the form to appear, it may take some time because it's done in JS $form = $crawler -> filter ( '#post-comment' ) -> form ([ 'new-comment' => 'Symfony is so cool!' ]); $client -> submit ( $form ); $client -> waitFor ( '#comments ol' ); // Wait for the comments to appear $this -> assertSame ( self :: $baseUri . '/news/symfony-live-usa-2018' , $client -> getCurrentURL ()); // Assert we're still on the same page $this -> assertSame ( 'Symfony is so cool!' , $crawler -> filter ( '#comments ol li:first-child' ) -> text ()); } }

Be careful, in test mode, environment variables must be defined by phpunit.xml.dist . Be sure to update DATABASE_URL to reference a clean database populated with the required tables. When the database is ready, run the tests:

1 $ ./bin/phpunit

Watch the Screencast

Thanks to Panther, you can take advantage of both your existing Symfony skills and the nice BrowserKit API to test modern JavaScript apps.