Sunday, 7 October 2007

Adding some BDD flavour to the PHPUnit framework

Influenced by an article written by David Astel and the latest 'interest resurrecting' blog entries of Pádraic Brady, I was looking for a way to bend over the PHPUnit test-centric vocabulary to a more behaviour-centric one. The motivation for this 'NLP' is best justified by a quote from David Astel's paper "... if you want to change how you think it can help to first change your language".

In Sebastian Bergmann's latest slides about Advanced PHPUnit Topics, he already provides a possibility to weaken the default test-centric vocabulary by using the annotation feature of his framework to enable the use of customizable and thereby freely nameable test/behaviour specifying methods.

The following code listing re-shows the use of the aforementioned annotation feature of PHPUnit to change the verb from test for verifying the system under development(SUD) to should for specifying the SUD.

<?php
require_once 'PHPUnit/Framework.php';

class CartSpecification extends PHPUnit_Framework_Testcase
{
/**
* @test
*/
public function shouldContainTwoProducts()
{
// only assert available
}
/**
* @test
*/
public function shouldBeEmptyAfterSuccessfullOrder()
{

}
/**
* @test
*/
public function shouldIncreaseAmountOnSameProductAddition()
{

}
}
Wrapping the assertations
When starting to specify the behaviour of the SUD i.e. a shopping cart PHPUnit provides a collection of assertations out of the box. As assert in the domain of testing means to verify something and as the goal of BDD is to promote/support a mindshift from verfication to specification this doesn't just feel natural, so I wanted to bend the verb assert over to the more appropriate sounding verb should. This can be achieved by writing an Adapter for the already existing Assert class of PHPUnit. The following listing outlines such an Adapter class i.e. called Expect, wrapping the already builtin assertations.
<?php
require_once 'PHPUnit/Framework/Assert.php';

class PHPUnit_Framework_Expect extends PHPUnit_Framework_Assert
{
public static function shouldNotInclude($needle, $haystack, $message = '')
{
self::assertNotContains($needle, $haystack, $message = '');
}
public static function shouldInclude($needle, $haystack, $message = '')
{
self::assertContains($needle, $haystack, $message = '');
}
public static function shouldEqual($expected, $actual, $message = '', $delta = 0, $maxDepth = 10)
{
self::assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10);
}
public static function shouldNotEqual($expected, $actual, $message = '', $delta = 0, $maxDepth = 10)
{
self::assertNotEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10);
}
...
}
Making the expectations available
To make use of the additional defined vocabulary it's necessary to create a class called Specification, which is nearly indentical to the PHPUnit_Framework_TestCase class and differs only in it's naming and the derivation from the aforedefined PHPUnit_Framework_Expect class instead of PHPUnit_Framework_Assert. The following code shows an excerpt of this class with the necessary adjustments.
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Framework/MockObject/Mock.php';
require_once 'PHPUnit/Framework/MockObject/Matcher/InvokedAtLeastOnce.php';
require_once 'PHPUnit/Framework/MockObject/Matcher/InvokedAtIndex.php';
require_once 'PHPUnit/Framework/MockObject/Matcher/InvokedCount.php';
require_once 'PHPUnit/Framework/MockObject/Stub.php';
require_once 'PHPUnit/Runner/BaseTestRunner.php';
require_once 'PHPUnit/Util/Filter.php';

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');

if (!class_exists('PHPUnit_Framework_Specification', FALSE)) {

abstract class PHPUnit_Framework_Specification extends PHPUnit_Framework_Expect
implements PHPUnit_Framework_Test,
PHPUnit_Framework_SelfDescribing
{
// same as PHPUnit_Framework_Testcase to maintain mockability etc.
...
}

}
At last the two new classes have to be addedd to the framework by adding two require statements to PHPUnit_Framework.php as shown next.
<?php

require_once 'PHPUnit/Util/Filter.php';

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');

require 'PHPUnit/Framework/SelfDescribing.php';
require 'PHPUnit/Framework/AssertionFailedError.php';
require 'PHPUnit/Framework/Assert.php';

require 'PHPUnit/Framework/Expect.php';

require 'PHPUnit/Framework/Error.php';
require 'PHPUnit/Framework/Notice.php';
require 'PHPUnit/Framework/IncompleteTest.php';
require 'PHPUnit/Framework/SkippedTest.php';
require 'PHPUnit/Framework/Test.php';
require 'PHPUnit/Framework/TestFailure.php';
require 'PHPUnit/Framework/TestListener.php';
require 'PHPUnit/Framework/TestResult.php';
require 'PHPUnit/Framework/ExpectationFailedException.php';
require 'PHPUnit/Framework/IncompleteTestError.php';
require 'PHPUnit/Framework/SkippedTestError.php';
require 'PHPUnit/Framework/SkippedTestSuiteError.php';
require 'PHPUnit/Framework/TestCase.php';

require 'PHPUnit/Framework/Specification.php';

require 'PHPUnit/Framework/TestSuite.php';
require 'PHPUnit/Framework/Warning.php';
require 'PHPUnit/Framework/Constraint.php';
require 'PHPUnit/Framework/ComparisonFailure.php';
?>
Using the new vocabulary
To make use of the new defined behaviour-centric vocabulary for specifying a SUD, it's specification 'driver' now has to be derived from the PHPUnit_Framework_Specification class.
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';
require_once 'Product.php';

class Cart extends PHPUnit_Framework_Specification
{

protected $cart = null;

protected function setUp()
{
$this->cart = new Cart();
}
/**
* @test
*/
public function shouldContainTwoProducts()
{
$this->cart->addProduct(new Product('SDT-10001', 'Product 1'));
$this->cart->addProduct(new Product('WRO-55000', 'Product 2'));

// should is available
$this->shouldEqual(2, sizeof($this->cart->getProducts()));

// assert still available as PHPUnit_Framework_Expect is derived from PHPUnit_Framework_Assert
$this->assertEquals(2, sizeof($this->cart->getProducts());
}
/**
* @test
*/
public function shouldBeEmptyAfterSuccessfullOrder()
{
...
}
/**
* @test
*/
public function shouldIncreaseAmountOnSameProductAddition()
{
...
}
}
To run the specification of the SUD the PHPUnit Cli is run as known and it's still possible to make use of it's testdox feature like in the following console excerpt.
C:\Apache2.2\htdocs\spec>phpunit --testdox Cart.php
PHPUnit 3.1.4 by Sebastian Bergmann.

Cart
- Should contain two products
- Should be empty after successfull order
- Should increase amount on same product addition
Of course this PHPUnit vocabulary 'hack' is far away from the featuresets provided by JBehave or RSpec, but maybe it's useful to someone until the first PHP BDD tools hit the community stage. Also make sure to keep the PHPSpec project on your radar if you're interested in BDD.

5 comments:

Pádraic Brady said...

Great post Raphael :). Bring on the BDD goodness to PHP! Is it possible to something like this as an extension? I know PHPSpec is coming, but many folk will simply want some of the language benefit applied to PHPUnit and SimpleTest.

Raphael Stolt said...

Thanks. I guess this might/must be possible, but I've to admit that at this time I have not a real 'deep' understanding of these two frameworks, that's why I consider the shown approach more as a sort of a hack. Seems like I have to digg a little deeper.

Bjarte said...

Great post!

Would it not be more 'bdd' ish to be able to do:

$this->argument(2)->shouldEqual(sizeof($array));

Not sure how feasible it is to implement it with PHPUnit but it "reads" better imho.

Raphael Stolt said...

Thanks. And yes you're totally right, it puts the focus on the SUD in a more clearly/readable way. But as you already indicated I guess(also not so sure) it would require much more effort, than just a simple 'vocabulary' wrapper for the PHPUnit_Assert class.

Keith said...

Interesting. I've taken a different approach and have begun writing a phpunit extension framework that runs off of scenario text files and uses regexp to map scenario statements to step class functions.


It's still in a vary early prototype phase but works excellently so far. I'll be posting source here(http://code.google.com/p/phpconform/) in the next few weeks once I have the time to polish the extensions further.