End-to-End Functional Testing is the process of testing interactions between multiple modules of an application from a "start to finish" perspective.
Since End-to-End tests represent real-world scenarios, the DSL (Domain-Specific Language) used to describe such scenarios needs to be as expressive as possible. It is therefore important to use tooling that is flexible enough to allow the creation of semantically meaningful tests.
In this article, we are going to build a basic DSL (similar to Capybara) on top of Kahlan and Mink to perform End-to-End tests. Here is the final result:
namespace testing\spec\suite;
describe("Google", function() {
it("displays the Google logo", function() {
browser()->visit('http://www.google.fr');
expect(element('#hplogo')->getAttribute('title'))->toBe('Google');
});
it("can search for and find Kahlan", function() {
browser()->visit('http://www.google.fr');
page()->fillField('q', 'Unit/BDD PHP Test Framework for Freedom, Truth, and Justice');
page()->pressButton('btnG');
wait(page())->toContain('Kahlan');
});
}, 10);
If you are familiar with Jasmine-like testing frameworks, you should have been able to understand the code above. Let's dig deeper to see how things as been wired up under the hood.
Note: you can find the code used in this article at github.com/jails/testing.
Configuring kahlan-config.php
Setting up the WebDriver server
To interact with browsers, we are going to need a WebDriver server like Selenium (or similar). For this part, we shall use the WebDriver Manager library. This library allows you to keep Selenium server binaries up to date and also provides an API for easily starting a Selenium server. To use it inside Kahlan, we are going to configure kahlan-config.php
like so:
use Peridot\WebDriverManager\Manager;
use box\Box;
use code\Code;
use code\TimeoutException;
use filter\Filter;
$box = box('spec', new Box());
$box->service('manager', function() {
$manager = new Manager();
$manager->update();
return $manager->startInBackground();
});
Filter::register('run.webdriver', function($chain) {
$process = box('spec')->get('manager');
try {
$fp = Code::spin(function() {
return @fsockopen('localhost', 4444);
}, 5, true);
fclose($fp);
} catch (TimeoutException $e) {
echo "Unable to run the WebDriver binary, abording.\n";
$process->close();
exit(-1);
}
return $chain->next();
});
Filter::apply($this, 'run', 'run.webdriver');
Note: In the code above, box()
is the rudimentary Dependency Injection Container included by default in Kahlan, but you can use your own implementation if you prefer.
The first step in the script above was to create the 'manager'
service, which runs the WebDriver server and returns the created process when invoked.
The second part of the script consists of configuring the 'run.webdriver'
closure to run just before the 'run'
step (i.e. before the test specs run).
Code::spin()
is a simple utility function which loops over a closure until a timeout is reached or until the executed closure returns a non-empty value (another approach to this could be using the sleep(x)
pattern).
Note: In this example we are going to use the Selenium driver, but since Mink supports a lot of different drivers, pick the driver that suits you best.
Setting up the Mink instance
In order to use Firefox to runs our suite of specs, we need to also setup Mink accordingly using the MinkSelenium2Driver driver.
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Mink;
use Behat\Mink\Session;
$box->service('mink', function() {
$selenium = new Selenium2Driver('firefox', null, 'http://localhost:4444/wd/hub');
$mink = new Mink(['firefox' => new Session($selenium)]);
$mink->setDefaultSessionName('firefox');
return $mink;
});
Filter::register('register.globals', function ($chain) {
$root = $this->suite();
$root->mink = $mink = box('spec')->get('mink');
$root->afterEach(function() use ($mink) {
$mink->resetSessions();
});
return $chain->next();
});
Filter::apply($this, 'run', 'register.globals');
In the script above, the 'register.globals'
filter has been created to inject the mink
variable to all scopes and make it available in all specs. Similarly, the closure passed to afterEach()
will be executed after each specs in the suite completes its run. This will help to keep specs isolated, without having to explicitly tear down state in each spec.
Registering a custom matcher
In Kahlan it's possible to register a matcher to be used only for a specific class name. This feature can be used in our example to match with Mink's Behat\Mink\Element\Element
instances. Note that all of these instances have a getText()
method, which returns the textual representation of an element. So, to avoid having to deal with getText()
everywhere, and in the interests of a consistent API, we can delegate these checks to a specific matcher:
use kahlan\Matcher;
Filter::register('register.matchers', function ($chain) {
Matcher::register('toContain', 'testing\spec\matcher\ToContain', 'Behat\Mink\Element\Element');
return $chain->next();
});
Filter::apply($this, 'run', 'register.matchers');
You can get the full matcher code here.
Post-suite cleanup
When the entire test suite has completed, the following script will clean things up properly:
Filter::register('cleanup', function ($chain) {
$box = box('spec');
$box->get('mink')->stopSessions();
$box->get('manager')->close();
return $chain->next();
});
Filter::apply($this, 'stop', 'cleanup');
Setting up the DSL
In order to write meaningful specs, we would also like to create a DSL similar to the Capybara one. To this end, we are going to write some helper functions to interact with the browser, the page and elements à la Capybara:
function browser($session = null)
{
return Suite::current()->mink->getSession($session);
}
function page($session = null)
{
return browser($session)->getPage();
}
function element($selector = 'body', $parent = null)
{
$parent = $parent ?: page();
$element = $parent->find('css', $selector);
return $element ?: new ElementNotFound($selector);
}
These helper functions are not exhaustive, and you can of course add whatever functions you need to the DSL.
Dealing with asynchronous expectations
When interacting with a browser, elements appear or disappear from a page asynchronously. To perform asynchronous tests, you can use the kahlan waitsFor()
statement, which runs a closure until some condition passes or until a timeout is reached.
We are going to use this waitsFor()
statement to create the following additional helper function:
function wait($actual, $timeout = 0)
{
return waitsFor(function() use ($actual) {
return $actual;
}, $timeout);
}
The last step is then to include the helper in the kahlan-config.php
file:
require __DIR__ . "/spec/api/helpers.php";
And you are done! You should now be able to configure your own environment to write End-to-End tests which fit your requirements.
Wrapping up
End to End tests are a vital part of any testing strategy. With a little configuration, Kahlan allows you to write your End to End, Unit and Characterisation tests within a consistent testing environment.