The Workshop: Specification BDD with Phpspec - php[architect] Magazine May 2020
Joe • January 10, 2021
learning phparch writing php testingWarning:
This post content may not be current, please double check the official documentation as needed.
This post may also be in an unedited form with grammatical or spelling mistakes, purchase the May 2020 issue from http://phparch.com for the professionally edited version.
Specification BDD with phpspec
phpspec is a package in which we can use behavior driven development, BDD, which comes fromtest driven development, TDD. When applying BDD we’ll write our tests first and then only enough code to pass our tests and then refactor and verify the tests still pass exactly as we would with TDD. This cycle continues and is often referred to as red-green development of writing tests that fail and enough code to make them pass, then restarting the process.
BDD is used at the story level and spec level also referred to as SpecBDD. The only real difference between TDD and SpecBDD is we’re using a spec tool, phpspec, to write tests with a descriptive specification language instead of a unit test tool such as PhpUnit where we would be writing simple comparisons of values via assertions. The idea behind using SpecBDD is to ensure the code we’re writing is confirming to the specification we write to define the function or feature.
Another tool often mentioned with BDD in the PHP ecosystem is Behat which is a StoryBDD tool focused on specifying feature narratives, what they require and depend on and what those mean to the rest of the application. SpecBDD is only focused on the implementation, how our code will deliver the features defined in the specifications.
Specification testing excels when the organization puts a lot of planning and research into how a system should behave and work. Usually, you’ll have very clearly defined features and it will be up to you to deliver high-quality code against the specifications of the features. Product owners or stakeholders will write up specs which could also be considered user stories and developers will translate those into code using phpspec.
Spec testing is different than unit testing because we’re not limiting our tests to one small piece, or unit, of our application. We’re testing all the specified requirements which have been given are matched by the application’s behavior. Feature and Acceptance testing are more similar to spec testing than unit testing because of the blurred lines of the code we want to test may not be limited to one function or unit. With acceptance or feature tests often a browser is involved and the testing tool will execute the application and apply the test suite to the running code to verify the application will behave exactly as expected. Specification testing is the formalization of this style of testing where we want to ensure everything works together so we’ll write a specification and then write enough code to fill the spec before continuing.
To demonstrate what phpspec can do we’re going to revisit an old favorite example application of mine: PHP Easy Math. We’re going to rewrite the package using SpecBDD.
Creating our project
We’re going to start with a clean project in a new folder and use Composer to install phpspec by running: $ composer require --dev phpspec/phpspec
Next we’ll setup our composer.json
file to use PSR-0 Autoloading which means we’ll add an autoload:
section as below:
{
“require-dev”: {
“phpspec/phpspec”: “^6.1”
},
“config”: {
“bin-dir”: “bin”
},
“autoload”: {
“psr-0”: {
“”: “src/“
}
}
}
This means we’ll put our application code in src/
folder and use the following conventions for our class names: \<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
. This style of autoloading shouldn’t be new to those who are familiar with modern PHP frameworks. The Composer website also has great documentation on autoloading. We’re also using the bin-dir
configuration option to tell Composer we’d like to have the phpspec
binary copied to bin/
instead of vendor/bin/
. This allows us to save characters each time we run phpspec
.
Understanding our specifications
Our easy math application consists of two classes, Addition
and Subtraction
. Each class has two methods which will take input and return output. The Addition
class will have an add
method and a sum()
method. The Subtraction
class will have subtract()
and sub()
methods.
The add()
method will take two float arguments and return the sum of the values. The sum()
method will take two or more float arguments and return the sum. The subtract()
method will take two float arguments and return the difference while the sub()
method will take at least two float arguments subtracting each and return the final value.
Diving into phpspec we want to describe our first class so we can begin our SpecBDD process of creating a minimal spec that will fail and then write enough code to make our spec pass.
$ ./bin/phpspec desc Addition
Specification for Addition created in spec/AdditionSpec.php.
Think of the /spec
folder as your traditional tests/
folder. This is where our specifications will go and be read from by phpspec. The desc
command scaffolds out the initial spec class for us:
<?php
namespace spec;
use Addition;
use PhpSpec\ObjectBehavior;
class AdditionSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Addition::class);
}
}
Our AdditionSpec
class will extend PhpSpec\ObjectBehavior
which allows us to call the class we are describing and verify the methods math the result of their output based on our expectations as described in our specification. The object behavior spec call is made up of public method “examples” which start with it_
or its_
and will be the methods executed by phpspec.
Shoutout to my PSR-1: Basic Coding Standard - PHP-FIG family
Specifically:Method names MUST be declared in camelCase.
i know the phpspec snake case looks weird, butlets_just_ride_with_it()
.
Our first example method asks us it_is_initializable()
. True to TDD form we start small and iterate, we want to ensure the Addition
class can be instantiated so we’ll throw an error if it can’t be. We can run our specs via ./bin/phpspec run
:
We can see our example is broken because when phpspec tried to instantiate our Addition
class it failed. After all, class Addition does not exist.
which makes perfect sense because we haven’t created it yet. phpspec offers to create the Addition
class for us so we’ll enter Y
and hit enter:
If you tell phpspec to create the class for you and you continue to see errors regarding not being able to load the class, or phpspec asking you to create it repeatedly, answer
N
to break out of phpspec and then runcomposer dump-auto
to force Composer to reread all the classes it can find based oncomposer.json
. Once complete you should be able to run phpspec and get past the class not found errors.
phpspec has created our src/Addition.php
class for us and now our example passes because it was able to instantiate the Addition
class. We just finished our first red-green loop and we’re ready to create our next example it_should_add_two_numbers_and_return_sum
:
function it_should_add_two_numbers_and_return_sum()
{
$this->add(3.14, 3.14)->shouldReturn(6.28);
}
We’ve written our specification which describes an add()
method taking two float values and should return the sum of the values. Now we run our spec with phpspec to see where we stand since we know we haven’t written our add()
method yet:
phpspec can see which our Addition
class does not have an add()
method and will offer to create the method for us. We’ll enter Y
and hit enter to continue running our specs:
Now we’ve hit red in our loop and need to write enough code to satisfy our specification which expects the add()
method to return the sum of the two values. We can add a small bit of logic to implement this feature:
<?php
class Addition
{
public function add($argument1, $argument2)
{
return $argument1 + $argument2;
}
}
So far the only code we’ve written in the Addition
class is our return statement: return $argument1 + $argument2;
. phpspec realized the class was missing and asked to create it and then did the same when it came to the specification of the add()
method. We’ll run our specs again now we believe we’ve implemented enough functionality:
Implementing our return statement brings us back to green. We have one specification for the Addition
class with two examples, one to verify the class can be instantiated and another to verify the add()
method takes two float values and returns their sum.
Continuing into our SpecBDD journey we need to create an example to demonstrate the sum()
method should be able to take multiple arguments and return their sum:
function it_should_take_multiple_numbers_and_return_sum()
{
$this->sum(3.14, 3.14)->shouldReturn(6.28);
$this->sum(3.14, 3.14, 3.14, 42)->shouldReturn(51.428);
}
We’ve added two checks to our example, one to verify the sum()
method will take two floats exactly like our add()
method and another to add four arguments together. We’ll run our specs and because the sum()
method is missing phpspec will offer to create it for us and try to run the broken spec again:
Once again we’re red in our red-green TDD workflow so we’ll implement enough logic to solve our examples. phpspec has created our sum()
method for us based on the spec we set it our example with two arguments. we want to get back to green so we solve the immediate problem of expected [double:6.28], but got null.
by adding return $argument1 + $argument2;
to the sum()
method. Running our specs again to get back to green however we see an error message:
The exception being thrown is because we’re passing four values into the sum()
method and we only account for two currently. Now it’s time to refactor our method to be able to account for variable-length arguments:
public function sum(…$numbers)
{
$sum = 0;
foreach ($numbers as $number) {
$sum += $number;
}
return $sum;
}
Running our specs will show we’re back to green:
We’ve made great progress with very little effort so far. While I’m confident our code is working I’m going to revise our Addition spec and add some more to our examples to verify the behavior is what we expect:
function it_should_add_two_numbers_and_return_sum()
{
$this->add(3.14, 3.14)->shouldReturn(6.28);
$this->add(0, 0, 0)->shouldReturn(0);
$this->add(0, 1)->shouldReturn(1);
$this->add(0, 1)->shouldReturn(1);
$this->add(1, 1)->shouldReturn(2);
$this->add(1337, 1337)->shouldReturn(2674);
}
function it_should_take_multiple_numbers_and_return_sum()
{
$this->sum(3.14, 3.14)->shouldReturn(6.28);
$this->sum(3.14, 3.14, 3.14, 42)->shouldReturn(51.42);
$this->sum(4, 10, 10, 8, 2, 5, 3)->shouldReturn(42);
$this->sum(0, 0, 0)->shouldReturn(0);
$this->sum(0, 1)->shouldReturn(1);
$this->sum(0, 1)->shouldReturn(1);
$this->sum(1, 1)->shouldReturn(2);
$this->sum(1337, 1337)->shouldReturn(2674);
}
The keen observer will recognize I pulled more checks from our previous PHP Easy Math library’s data providers. We also added a check where we passed in 7 values and returned the proper sum. While phpspec does not natively support data providers there are a couple of projects on Github but I wasn’t able to find one which looked active. I’m going to settle to have a lot of checks in my examples instead of neat data providers.
Now confident in our code spec coverage we’re ready to move on to the Subtraction class and we’ll start our red-green loop by using phpspec’s desc
command:
shell
$ ./bin/phpspec desc Subtraction
Specification for Subtraction created in /Users/halo/Code/BDD-Easy-Math/spec/SubtractionSpec.php
Now we know from our AdditionSpec
we’re going to have 3 examples testing various conditions to ensure our Subtraction
class matches the SubtractionSpec
we’re about to create. This time around the loop we’re going to fill out our specification class and then run our spec to see where we stand. Ultimately we’ll be skipping a few red-green loop cycles.
Our full SubtractionSpec
class looks very similar to our AdditionSpec
, we’ve just updated the values and methods to perform the same checks:
<?php
namespace spec;
use PhpSpec\ObjectBehavior;
use Subtraction;
class SubtractionSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Subtraction::class);
}
function it_should_add_two_numbers_and_return()
{
$this->subtract(3.14, 6.28)->shouldReturn(-3.14);
$this->subtract(0, 1)->shouldReturn(-1);
$this->subtract(1, 0)->shouldReturn(1);
$this->subtract(1, 1)->shouldReturn(0);
$this->subtract(2674, 1337)->shouldReturn(1337);
}
function it_should_take_multiple_numbers_and_return_sub()
{
$this->sub(3.14, 6.28)->shouldReturn(-3.14);
$this->sub(0, 1)->shouldReturn(-1);
$this->sub(1, 0)->shouldReturn(1);
$this->sub(1, 1)->shouldReturn(0);
$this->sub(2674, 1337)->shouldReturn(1337);
$this->sub(51.42, 1337, 3.14, 42)->shouldReturn(-1330.72);
$this->sub(4, -10, 10, 8, -2, 5, 35)->shouldReturn(-42);
$this->sub(0, 0, 0)->shouldReturn(0);
$this->sub(1, 0)->shouldReturn(1);
$this->sub(0, 1)->shouldReturn(-1);
$this->sub(1, 1)->shouldReturn(0);
$this->sub(2674, 1337)->shouldReturn(1337);
}
}
Running our specification we see two problems as we expect: missing methods for sub
and subtract
. We’ll tell phpspec to go ahead and create these methods:
Now we know we’re back to red because we haven’t implemented the methods which were created by phpspec. We’ll implement those methods with the following code:
public function subtract($x, $y)
{
return $x - $y;
}
public function sub($firstNumber, …$numbers)
{
$sub = $firstNumber;
foreach ($numbers as $number) {
$sub -= $number;
}
return $sub;
}
Running phpspec shows us we’re back to green:
IIt’s important to note we skipped ahead a good bit. By using our domain knowledge of the application and how it should behave we were able to shorten the red-green TDD loop significantly. If you’re new to TDD or BDD I highly caution you against skipping too far head in each loop unless you clearly understand all the nuances and tradeoffs of the feature your building. You may even want to slow the process down in some cases to ensure you are comprehending the spec and the implementation of the domain-driven by the spec as it was intended. Every time I’ve used this form of strict TDD my loop time always varies and is ultimately a balance between shortening the loops where I can without feeling like I’m overlooking or skipping a piece of the specification. You may need to spend some time to figure out the best pace for yourself.
Now that we’ve rebuilt our PHP Easy Math library from scratch via SpecBDD and we have our examples passing locally we should wire up Github Actions to run phpspec for us automatically.
We’re going to use a similar configuration for Github actions as we’ve previously covered in The Workshop: GitHub Actions for Continuous Integration | phparchitect. We’ll create the file .github/workflows/php.yml
with the following contents:
name: PHP Composer
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [‘7.4’, ‘7.3’, ‘7.2’]
name: PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v1
- name: Install PHP
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php }}
- name: Report PHP version
run: php -v
- name: Validate composer.json and composer.lock
run: composer validate
- name: Get Composer Cache Directory
id: composer-cache
run: echo “::set-output name=dir::$(composer config cache-files-dir)”
- name: Cache dependencies
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.php }}-composer-${{ hashFiles(‘**/composer.lock’) }}
restore-keys: ${{ matrix.php }}-composer-
- name: Install dependencies
run: composer install —prefer-dist —no-progress —no-suggest
- name: Run test suite
run: bin/phpspec
You can review our Github Actions runs via Actions · svpernova09/BDD-Easy-Math · GitHub. The actions will instruct Github to test our code against the three most recent versions of PHP, using Composer to install our dependencies and we’re reusing some other Github actions to verify and validate our composer.json
file.
Happy (SpecBDD) Testing!
Warning:
This post content may not be current, please double check the official documentation as needed.
This post may also be in an unedited form with grammatical or spelling mistakes, purchase the May 2020 issue from http://phparch.com for the professionally edited version.
As Seen On
Recent Posts
- PHP to Rust via Copilot
- Compiling Python 3.12 from Source Tarball on Linux
- HTML Form Processing with PHP - php[architect] Magazine August 2014
- The Workshop: Local Dev with Lando - php[architect] Magazine November 2022
- Getting back into the swing of things
- All Posts
Categories
- ansible
- apache
- applesilicon
- aws
- blackwidow
- cakephp
- community
- composer
- conferences
- copilot
- data-storage
- day-job
- devops
- docker
- fpv
- general
- github
- givecamp
- homestead
- jigsaw
- joindin
- keyboard
- laravel
- learning
- linux
- maker
- mamp
- mentoring
- music
- nonprofit
- opensource
- packages
- php
- phparch
- projects
- provisioning
- python
- razer
- rust
- s3
- security
- slimphp
- speaking
- static-sites
- storage
- testing
- tiny-whoop
- today-i-learned
- training
- ubuntu
- vagrant
- version-control
- windows
- writing
- wsl
- wsl2
- zend-zray