Automated testing is big news these days. There's hardly a PHP conference happening without a talk on testing automation or derivative methodologies. TDD (Test-Driven Development) and BDD (Behaviour Driven Design) are all around us. So why should you care about all this? Well, there are many excellent reasons to do automated testing, including assuring application quality and inspiring developer confidence in a system. If you are a business person, you're most likely to care about the quality; if you're a developer then the confidence aspect is more important. The more complex an application becomes, the harder it is to be sure that each new feature or bug fix won't break the system, and that decreases your overall confidence in your work as developer. That's exactly the reason why you need automated testing - to be confident that you're not breaking important parts of an application. Now you're convinced that automated testing is important, but isn't unit testing enough? Unit tests are cheap, fast and small. Why might you want to expand into using a technique such as functional testing? Once again the answer is confidence. The more complicated an application becomes, the more complicated the interactions between separate parts of the application become. Just as you can't be confident your car is roadworthy by manually turning each of its wheels independently, you can't be sure that an entire application is working by testing each of its units independently. You need functional testing for complex applications - and today, that's every application.
Silex is a PHP microframework based on Symfony2 components. It has rapidly become a popular tool in the Symfony2 community thanks to its simplicity and shallow learning curve. That said, the simplicity of Silex does not mean that it is only fit for simple applications, in fact the opposite is true. Some types of application, like RIAs (Rich Internet Applications) are better built with PHP and the minimalism of Silex.
As an application becomes more complex, so it becomes more important to properly test it. Silex provides some tools for functional testing using the Symfony2 BrowserKit component. This is useful, but what if some of of the application's functionality requires real HTTP requests to be made? Those could be done using a console browser emulator like Goutte. How about if some functions depend on AJAX to behave properly? Using Selenium or even Zombie.js could be a good option in that case.
There are two problems with all the solutions mentioned here:
- There's simply no single best choice in terms of browser emulation. Some emulators (BrowserKit, Goutte) are extremely fast, but don't support AJAX. Others (Zombie.js, Selenium) support AJAX but are much slower. In an ideal world, you would want to use multiple emulators at the same time, choosing the best one for each specific case. That leads us to problem #2.
- Different browser emulators are written in different languages (PHP, JS, Java), using different libraries and provide extremely inconsistent APIs for developers to work with. That means it is difficult to switch from one emulator to another as it will require all helpers and testing tools be rewritten.
To help solve some of these problems, a library called Mink was written. It was created originally as a complementary tool for Behat, but is not tightly coupled to Behat itself and so it can also be used with any framework or tool out there. Mink is a browser emulator abstraction layer - a tool that removes the differences between the various browser emulators and provides one single API through which you can control them all. This essentially means you can switch from browser emulator to browser emulator quite seamlessly as they all use the same PHP API. This article will show you how to use Mink to functionally test an example Silex application using a couple of different browser emulators.
As our test application we'll use a very simple Silex blog application that is already prepared for you. The code is on github at https://github.com/everzet/silex-mink so use that as your starting point.
Set Up the Test Suite
In order to install and integrate Mink, Silex and PHPUnit, some extra entries are needed in composer.json. The new section should look something like this:
These changes ask Composer to install the latest versions of PHPUnit, Mink and couple of Mink drivers in the "dev" environment. To effect these changes, run the following command:
$> php composer.phar update --dev --prefer-dist
Drivers are what gives Mink knowledge about different browser emulators (Symfony2 BrowserKit and Selenium, in this case). Mink supports quite a selection of drivers out of the box:
- BrowserKit - Symfony2 TestClient driver. Extremely fast headless emulator without isolation or AJAX support.
- Goutte - written in PHP browser emulator. Fast fully isolated headless emulator that doesn't have AJAX support.
- Sahi - Sahi.js driver. Extremely slow real browser controller. Has best AJAX support in town.
- Selenium2 - Selenium v2 driver. Slow real browser controller. Has decent level of AJAX support.
- Zombie.js - Zombie.js driver. Fast headless browser emulator with AJAX support. Known to be buggy in some cases.
For demonstration purposes, we'll install only the BrowserKit and Selenium2 drivers, however the technique to switch between them is the same for switching to any other Mink driver.
Next step is to create our PHPUnit test configuration. Put inside your phpunit.xml:
Nothing really special here, except maybe the BASE_URL server variable. We'll use this in our test cases as this is a test suite environment detail.
Base Mink test caseBASE MINK TEST CASE
Now we're ready to start writing the first Mink test case for the Silex app. First let's create a base MinkTestCase class, which can be extended by each of the functional test classes. I put the following code into tests/functional/MinkTestCase.php:
The main area of interest at this point is the setUpBeforeClass() method. This method sets up Mink with two preconfigured browser sessions (one Silex, one Selenium) backed by BrowserKit and Selenium2 drivers. We also set a Silex session as the default, which means that when no $name argument is provided to the helper methods, they will work with this exact session.
The other 3 methods are helper methods which will be very useful when writing test cases:
Writing the first test case
With a preconfigured base TestCase class, the first test can now be written. Let's test that we can see and fill in the article form. In my project I added this to tests/functional/ArticleTest.php:
Let's work through the code snippet above:
- Visit the application homepage using the Session::visit() method and the BASE_URL.
- Find the article form using CSS identifiers, and assign the resulting DOM object to a variable called $articleForm.
- Make an assertion that a field with an attribute 'id', 'value', 'label' or 'name' containing 'Title' exists inside the form stored in $articleForm
- Do the same for a 'Body' field.
Now invoke PHPUnit to run the new testcase:
The test completes quickly; this is because by default it runs against the Silex browser session, which uses the BrowserKit driver, which in turn uses the internal Silex TestClient.
Were you expecting something more complicated? Let's add another test case, but this time filling in the fields on the form and checking that a preview area is shown:
Again, let's work through the example in order:
- Visit the /add-article page.
- Fill the 'Title' field with a value of 'Hello Mink!'.
- Fill the 'Body' field with 'Func testing is fun!'.
- Then press the 'Preview' button.
- Assert that element identified by a specific CSS selector contains some text.
Run the entire test suite again:
For completeness we should also test that preview area is hidden by default, which is fairly simple:
Hopefully the examples have seemed quite straightforward so far. Perhaps you are wondering though whether this could have been done with a default Silex function test case?
In fact, we can't quite do it with the other tools. One major benefit of Mink is a much cleaner API, which is consistent across all browser emulators. This potentially means that both our tests could be checked against different browsers with minimum effort.
Remember that optional $name argument to the helper methods? By providing "selenium" as an argument, it is possible to force Mink to use the Selenium session and Selenium2 driver for each written instruction. Multiple sessions and drivers can be mixed inside a single test case. You can even have multiple sessions using same driver working together to test, for example, a chat application.
For demonstration purposes let's switch all of our test cases to use Selenium2. How? By changing the default session name. Add this to your test class:
At this point, you should download and run the Selenium Server JAR from the Selenium website and then run the test suite again:
Then you run the tests this time, an actual browser (firefox by default) should have opened and performed all the actions in the tests. That's the power of Mink. It works exactly the same way for any Mink driver, such as Zombie.js or Sahi.js.
Are you using Mink? Would you like to? Tell us what you think in the comments - and happy testing!