I’ll be honest, I don’t do much testing. When it’s really necessary and I’m working on big enterprise projects, I do, but in general, my personal projects are usually one-man-army proofs of concept, or fixes on already tested apps.
We’ve done our share of testing posts here at SitePoint, with more coming soon, but I wanted to show you a relatively new testing tool I found that caught my attention because of how unconventional it seemed.
Peridot
Peridot is a BDD testing framework (so behavior driven testing) but for your units of code – not your business logic.
Wait, what?
Yes.
If you’re familiar with Behat, you’ll recognize this syntax (it should be fairly readable even if you’re not familiar with it):
Feature: adding a todo
As a user
I want my todos to be persisted
So I don't have to retype them
Scenario: adding a todo
Given I am on "/"
When I fill in "todo" with "Get groceries"
And I press "add"
And I reload the page
Then I should see "Get groceries"
Scenario: adding a duplicate todo
Given I have a done todo "Pick up dinner"
And I am on "/"
When I fill in "todo" with "Pick up dinner"
And I press "add"
Then I should see "Todo already exists" after waiting
And I should see 1 "#todos li" elements
The individual phrases are defined in FeatureContext classes like so:
/**
* @Then I should see :arg1 after waiting
*/
public function iShouldSeeAfterWaiting($text)
{
$this->getSession()->wait(10000, "document.documentElement.innerHTML.indexOf('$text') > -1");
$this->assertPageContainsText($text);
}
/**
* @Given I have a todo :arg1
*/
public function iHaveATodo($todoText)
{
$collection = self::getTodoCollection();
$collection->insert(['label' => $todoText, 'done' => false]);
}
The framework recognizes them, substitutes the arguments for their values, and tests the conditions.
But these are called “user stories” – they are what the user does, or is expected to do. Behat will literally open a browser when needed, test things, and report back. How could we possibly apply this to units of code?
Like so:
describe('ArrayObject', function() {
beforeEach(function() {
$this->arrayObject = new ArrayObject(['one', 'two', 'three']);
});
describe('->count()', function() {
it('should return the number of items', function() {
$count = $this->arrayObject->count();
assert($count === 3, 'expected 3');
});
});
});
In a similarly story-based style with cascading nested contexts, Peridot will “describe” and ArrayObject
‘s count()
method (notice how it first describes the ArrayObject, then describes the method later), and add an it
for an explanation on what the described entity should do.
The nested context approach also allows you to execute some bootstrapping logic before each entity’s creation, or before each sub-entity’s creation, making the testing framework incredibly flexible. That’s what the beforeEach
function does – it initiates a new ArrayObject
with three elements, so the count is always executed on the proper instance.
Ok so… what? It’s just an alternative syntax for PHPUnit? Well, yes and no. While it is focused on units and being able to test them in a story-based way, you can also see it as a broker language between clients and developers. That’s what makes Behat so appealing, too – if you give a Behat feature to a client, they will immediately be able to tell what it means and what it’s supposed to test. With a little bit of training, they’ll even be able to write more stories for you – and clients writing the tests for their own app is the holy grail of every developer’s project, I’m sure you agree.
Peridot is similar in that it offers a human readable way to test units – it is still PHP syntax, but its phrasing and nesting make it user-friendly enough to be understood by the client. And the phrasing also lends itself well to some really verbose reporting:
Concurrency
Peridot also offers “just works” concurrency with the concurrency plugin.
This is very handy when you’re testing with database connections, remote calls, or browser actions – all those are very slow, and executing them one after the other would take ages on any system. Instead, the concurrency plugin makes sure tests are executed in separate worker processes, spawned as needed. Once done, each worker communicates back to the parent process with the results, speeding up testing dramatically. There’s no configuration necessary, other than activating a plugin and passing along a flag (--concurrent
and --processes
) when launching the tests.
It’s important to note that one should be careful about tests that might depend on one another in such cases – for example, if you’re executing tests that do something in a database, it wouldn’t make sense to have them all do something on the same database, because they’d probably end up in conflicts. Instead, the concurrency plugin makes available the ID of the process that’s currently executing the spec, which makes it easy to create process-specific databases during tests, keeping all specs independent and safe:
$id = getenv('PERIDOT_TEST_TOKEN');
$dbname = "mydb_$id";
Focus and Skip
To rival PHPUnit, Peridot also offers a way to completely skip some tests, and to only run others. If you prefix a function with f
, the spec being tested will become focused. That’s a fancy way of saying only that test will be executed, and everything not f
-ed in that suite will not run. Helpful when fine tuning a test.
<?php
describe('A suite with nested suites', function() {
fcontext('A focused suite', function() {
it('should execute this spec', function() {
// ...
});
it('should also execute this spec', function() {
// ...
});
});
describe('An unfocused suite', function() {
it('should not execute this spec', function() {
// ...
});
});
});
In other cases, you’ll want to skip a test and mark it as temporarily unimportant or work-in-progress. When would you do this? For example, when a particularly awful implementation of the “drop-in-replacement-but-not-really-drop-in-if-we-are-being-honest” JSON extension for PHP broke my Diffbot PHP Client tests, I could do little else but mark it as skipped and wait for an extension that actually worked. Prefixing with x
in Peridot is the same as marking the test skipped in PHPUnit.
xdescribe('A pending suite', function() {
xcontext('when using a pending context', function() {
xit('should have a pending spec', function() {
// ...
});
});
});
These two features in combination are able to rival similar features of other testing frameworks.
Events and Plugins
Peridot is highly extensible via plugins and events that fire at certain stages of the testing process.
The events can be listened for in the main peridot.php
file, and their full list is available here. With such an open architecture, all sorts of plugins can be easily developed. The Yo Plugin, for example, will send a Yo! notification via the Yo service when tests pass or fail (depending on yo [sic] configuration). The Watcher plugin will monitor your test files and re-run the suite when changes have been detected (compatibility depends on OS – please use non-Windows. Homestead Improved would be best).
Code Coverage
With a powerful event architecture, simple syntax, and effective structure, Peridot also lends itself perfectly to integration with traditional code coverage reporting tools.
Here’s how to integrate it with PHPUnit’s code coverage report.
Custom DSL
If you don’t like the describe-it
syntax and aim to replicate something that feels more at home, or something more fitting for your business case, custom DSLs (Domain Specific Languages) can be developed as a replacement or augmentation. With custom DSLs, this becomes possible in Peridot:
Feature("chdir","
As a PHP user
I need to be able to change the current working directory",
function () {
Scenario(function () {
Given('I am in this directory', function () {
chdir(__DIR__);
});
When('I run getcwd()', function () {
$this->cwd = getcwd();
});
Then('I should get this directory', function () {
if ($this->cwd != __DIR__) {
throw new \Exception("Should be current directory");
}
});
});
});
A full example of how to make this happen is available here.
Custom Scopes
It is also possible to natively extend the scope of a test. For example, if we wanted to be able to use a webdriver (web browser for testing URLs) in our tests, we could activate it by installing WebDriver like so:
composer require facebook/webdriver peridot-php/webdriver-manager
Then, you create a class that extends Peridot’s Scope, so it can be tied into the tests:
<?php // src/Example/WebDriverScope
namespace Peridot\Example;
use Evenement\EventEmitter;
use Peridot\Core\Scope;
class WebDriverScope extends Scope
{
/**
* @var \RemoteWebDriver
*/
protected $driver;
/**
* @var \Evenement\EventEmitter
*/
protected $emitter;
/**
* @param \RemoteWebDriver $driver
*/
public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter)
{
$this->driver = $driver;
$this->emitter = $emitter;
//when the runner has finished lets quit the driver
$this->emitter->on('runner.end', function() {
$this->driver->quit();
});
}
/**
* Add a getPage method to our tests
*
* @param $url
*/
public function getPage($url)
{
$this->driver->get($url);
}
/**
* Adds a findElementById method to our tests
*
* @param $id
* @return \WebDriverElement
*/
public function findElementById($id)
{
return $this->driver->findElement(\WebDriverBy::id($id));
}
}
This is then activated in the main peridot.php
file:
<?php // peridot.php
use Peridot\Core\Suite;
use Peridot\Example\WebDriverScope;
require_once __DIR__ . '/vendor/autoload.php';
return function($emitter) {
//create a single WebDriverScope to port around
$driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', array(
WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX
));
$webDriverScope = new WebDriverScope($driver, $emitter);
/**
* We want all suites and their children to have the functionality provided
* by WebDriverScope, so we hook into the suite.start event. Suites will pass their child
* scopes to all child tests and suites.
*/
$emitter->on('suite.start', function(Suite $suite) use ($webDriverScope) {
$scope = $suite->getScope();
$scope->peridotAddChildScope($webDriverScope);
});
};
… and suddenly available in tests!
<?php
describe('The home page', function() {
it('should have a greeting', function() {
$this->getPage('http://localhost:4000');
$greeting = $this->findElementById('greeting');
assert($greeting->getText() === "Hello", "should be Hello");
});
});
Notice how we can even fully replicate Behat’s behavior with this!
More info about scopes here.
Conclusion
Peridot is a way to make your units of code come to life, following in the steps of the app itself when it’s being BDD-tested. On its own, it’s a great unit testing tool, but in tandem with something like Behat, it can cover the whole app rather nicely.
The describe-it
syntax is a bit odd to us who aren’t familiar with other tools using it, but it matches the context in which it’s being executed (describing the units) so meaning-wise, it works rather well. Besides – one can make their own DSL if describe-it
doesn’t feel good.
Including Peridot, we now have four major players on the testing market: PHPUnit, Behat, PHPSpec, and Peridot.
Which of these do you use? Why? Do you use a combination? If so, why that specific combination? Can you show us examples? We’d love to take a look at your setup and your code. Tell us in the comments below!
The code examples above were taken from Peridot’s todo app example
Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.