PhpSpec is a testing tool based on the concept of emergent design using specification. You may have heard of Behavior Driven Development (BDD), and PhpSpec is a tool to use at the spec level or SpecBDD. We also mentioned PhpSpec in our recent roundup of Laravel Testing Resources, which includes ways you can incorporate PhpSpec into your Laravel workflow if you are interested in trying out SpecBDD.
If you’ve never tried PhpSpec, one of the things that I love about PhpSpec is that the process generates code for you and guides you through the SpecBDD process through the command line. Let me show you what I mean with a quick tutorial.
Getting Started
If you want to follow along, make sure you have Composer installed and in your path. The first thing we’re going to do is create a new project. When I am tinkering with libraries, I like to create a project in a folder I call “Sandbox”:
1$ mkdir -p ~/Code/sandbox/calculator-phpspec && cd $_2$ mkdir src/34$ composer init5$ composer require --dev phpspec/phpspec
Next, let’s add a PSR-4 namespace to our project’s composer.json file, in the end, your file will look similar to this:
1{ 2 "name": "predmond/calculator-phpspec", 3 "require": {}, 4 "require-dev": { 5 "phpspec/phpspec": "^4.0" 6 }, 7 "autoload": { 8 "psr-4": { 9 "Predmond\\Calculator\\": "src/"10 }11 }12}
Before we forget, let’s dump the autoloader so our namespace can be used later:
1$ composer dump-autoload2Generating autoload files
You should be able to run the phpspec CLI now:
1# I like to have /vendor/bin in my path 2export PATH="./vendor/bin:$PATH" 3 4# Or run it with the path 5$ ./vendor/bin/phpspec 6phpspec 4.0.3 7 8Usage: 9 command [options] [arguments]10...
Configuring Our Suite
PhpSpec will look for a configuration file in the root of the project for a phpspec.yml
file. We can use this file to configure test suites, formatters, and other configuration rules.
The configuration documentation is pretty extensive, so you should check it out, but for our purposes, we just want to configure our PSR-4 namespace so I can walk you through the workflow of using PhpSpec to drive out a specification.
PhpSpec uses a top-level YAML key called “suites” so let’s define a default suite for our project’s namespace by creating a phpspec.yml file in the root of our project:
1suites:2 default:3 namespace: Predmond\Calculator4 psr4_prefix: Predmond\Calculator5formatter.name: pretty
We can use this suite to create a calculator class and drive out a basic calculator spec. Let’s get to work!
The Workflow
Our workflow with PhpSpec will typically look like the following steps:
- Describe a Specification
- Run the Specification (Create the class if it doesn’t exist)
- Write an expected behavior
- Run the specification (create the method if it doesn’t exist)
- Write the implementation
- Verify the behavior
- Repeat
The way PhpSpec works does a good job of forcing you into this BDD workflow, which you might fight at first, but tends to be a reliable coding workflow once you get comfortable. Let’s go through a quick example of the workflow so you can give it a shot!
Describing a Spec
The first thing we need to do is define a specification. In our example, we are going to describe a calculator. You define a specification (think of it as creating a test class) by using the describe
command. Run the following to create a Calculator spec:
1$ ./vendor/bin/phpspec describe Predmond/Calculator/Calculator23# Or you can use backslashes with quotes4$ ./vendor/bin/phpspec describe "Predmond\Calculator\Calculator"
Running this command creates our specification file in spec/CalculatorSpec.php
which looks like the following:
1<?php 2 3namespace spec\Predmond\Calculator; 4 5use Predmond\Calculator\Calculator; 6use PhpSpec\ObjectBehavior; 7use Prophecy\Argument; 8 9class CalculatorSpec extends ObjectBehavior10{11 function it_is_initializable()12 {13 $this->shouldHaveType(Calculator::class);14 }15}
Take note that we haven’t created the actual Calculator class yet. Our file includes one specification file (it_is_initializable
)—PhpSpec uses it_
and its_
with public functions to find specifications.
Also, take note of the underscore method name convention, which is an opinionated style that tends to be more readable. One goal of BDD is using language friendly to humans to describe behavior.
Now that we have our specification, let’s use run
to run our specs:
1$ ./vendor/bin/phpspec run 2 Predmond\Calculator\Calculator 3 4 11 ! is initializable 5 class Predmond\Calculator\Calculator does not exist. 6... 71 specs 81 examples (1 broken) 918ms1011Do you want me to create `Predmond\Calculator\Calculator` for you? [Y/n]
Enter “Y” and PhpSpec will create the Calculator
class. Rerunning the specifications should make our it_is_initializable
spec pass:
1$ ./vendor/bin/phpspec run23 Predmond\Calculator\Calculator45 11 ✔ is initializable671 specs81 examples (1 passed)99ms
You’ve just described your first specification, and PhpSpec took care of the details of creating methods and code for us! Let’s dig into creating more specifications.
Creating an Addition Specification
Let’s drive out a design for addition in our Calculator class. Our specifications are green, so we are ready to write a new specification before implementing it. Open up the spec/CalculatorSpec.php
file and add the following:
1function it_can_add_two_numbers()2{34}
I didn’t add any code because I want to show you how PhpSpec guides you on what to do next, which is pretty neat:
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 - can add two numbers 7 todo: write pending example 8 9101 specs112 examples (1 passed, 1 pending)1211ms
PhpSpec is telling us that we have a pending specification. Let’s drive it out now:
1function it_can_add_two_numbers()2{3 $this->add(2,3)->shouldReturn(5);4}
We are using the specification to say: “call add() with the parameters 2
, and 3
, and add() should return 5
.
Here’s an important point to understand: $this
is the object we are describing. You don’t need to construct the object as you would in other test suites:
1# Other test suites2$calculator = new Calculator();34# PhpSpec $this is the Calculator5$this->add(); // And string some matching expectations...
If we try to run our specification now, PhpSpec will tell us the next step:
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 ! can add two numbers 7 method Predmond\Calculator\Calculator::add not found. 8 9---- broken examples1011 Predmond/Calculator/Calculator12 16 ! can add two numbers13 method Predmond\Calculator\Calculator::add not found.1415161 specs172 examples (1 passed, 1 broken)1811ms1920 Do you want me to create21 `Predmond\Calculator\Calculator::add()` for you? [Y/n]
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 ✘ can add two numbers 7 expected [integer:5], but got null. 8 9---- failed examples1011 Predmond/Calculator/Calculator12 16 ✘ can add two numbers13 expected [integer:5], but got null.1415161 specs172 examples (1 passed, 1 failed)1811ms
PhpSpec will define the add()
method on our implementation if we confirm “Y”. Kind of cool, eh?
Once you confirm, immediately our specification fails. That means it’s time to write the minimal amount of code to get the specification to pass.
Our Calculator
class has been generated by PhpSpec at this point. Here’s what it should look like:
1<?php 2 3namespace Predmond\Calculator; 4 5class Calculator 6{ 7 public function add($argument1, $argument2) 8 { 9 // TODO: write logic here10 }11}
Based on our it_can_add_two_numbers()
specification, PhpSpec has created a public add()
method with two arguments defined. Let’s do the minimal amount to get this test passing:
1public function add($argument1, $argument2)2{3 return 5;4}
Most of the time doing this minimal amount of code is ridiculous but stick with me because when you test more complex specification writing the minimum amount of code can help you build a robust specification.
Now that we’ve written the minimal amount of code, it’s time to rerun our specs:
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 ✔ can add two numbers 7 8 91 specs102 examples (2 passed)1120ms
We have a passing spec! It’s not very useful—but it’s green.
Let’s add a few more examples to our specification to drive the implementation:
1function it_can_add_two_numbers()2{3 $this->add(2, 3)->shouldReturn(5);4 $this->add(10, 0)->shouldReturn(10);5 $this->add(0, 0)->shouldReturn(0);6 $this->add(1, -2)->shouldReturn(-1);7}
If you rerun your specs, our addition spec will fail in a blaze of glory. We are ready to update our implementation to get things back to green now:
1public function add($argument1, $argument2)2{3 return $argument1 + $argument2;4}
Running phpspec
should result in both specifications passing.
Division
Let’s quickly write a more interesting specification: preventing a division by zero error. When this happens, we will throw an exception.
The specification might look like this:
1function it_can_divide_two_numbers()2{3 $this->divide(10, 2)->shouldReturn(5);4}56function it_throws_a_division_by_zero_excption()7{8 $this->shouldThrow('\Predmond\Calculator\DivisionByZeroException')->duringDivide(10, 0);9}
And the implementation:
1public function divide($argument1, $argument2)2{3 return $argument1 / $argument2;4}
If you run the specs you should get a division by zero warning:
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 ✔ can add two numbers 7 24 ✔ can divide two numbers 8 29 ✘ throws a division by zero exception 9 expected exception of class "\Predmond\Calculator\Divi...", but got10 [exc:PhpSpec\Exception\Example\ErrorException("warning: Division by zero in11 /Users/predmond/Code/sandbox/calculator-phpspec/src/Calculator.php line 14")].
Let’s fix that by throwing the exception when the denominator is zero:
1public function divide($numerator, $denominator)2{3 if ($denominator === 0) {4 throw new DivisionByZeroException();5 }67 return $numerator / $denominator;8}
And make sure to create the exception class in the src/
folder:
1<?php23namespace Predmond\Calculator;45class DivisionByZeroException extends \Exception {}
And running our specs should pass now:
1$ ./vendor/bin/phpspec run 2 3 Predmond\Calculator\Calculator 4 5 11 ✔ is initializable 6 16 ✔ can add two numbers 7 24 ✔ can divide two numbers 8 29 ✔ throws a division by zero exception 910111 specs124 examples (4 passed)1313ms
Constructing Objects and Matchers
There are two important concepts I want to introduce before we wrap up: constructing objects and matchers.
Our Calculator class is pretty basic, but you will likely need to construct objects that have dependencies. You should read the object construction section to get an overview.
Matchers are used in PhpSpec to describe how an object should behave. You’ve already used the identity matcher when you called $this->add(2, 3)->shouldReturn(5);
. The shouldReturn
uses the identity operator (===
) to make sure the spec matches expectations. The four types of identity matchers you can use are:
1$this->calculatorMemory()->shouldBe(5);2$this->getTitle()->shouldBeEqualTo("Star Wars");3$this->getReleaseDate()->shouldReturn(233366400);4$this->getDescription()->shouldEqual("Inexplicably popular children's film");
You’ve also used the Throw Matcher when we expected our divide()
method to throw a DivisionByZeroException
exception.
You can check out the full matchers documentation and use it as a reference to work with PhpSpec.
Learn More
We’ve just scratched the surface of what PhpSpec can do. We didn’t demonstrate collaborators, but perhaps we can do a follow-up post with more examples. You should read through the whole PhpSpec manual, which isn’t very long. Once you get a handle on things, learn about Prophet Objects which include stubs, mocks, and spies.
I have a small HTML to AMP library that uses PhpSpec (and uses TravisCI). Mike Stowe has an excellent introductory article called What is Spec Driven Development that will give you an overview of SpecBDD.
Filed in:
Full stack web developer. Author of Lumen Programming Guide and Docker for PHP Developers.