Testing With PhpSpec

Published on by

Testing With PhpSpec image

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”:

$ mkdir -p ~/Code/sandbox/calculator-phpspec && cd $_
$ mkdir src/
 
$ composer init
$ 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:

{
"name": "predmond/calculator-phpspec",
"require": {},
"require-dev": {
"phpspec/phpspec": "^4.0"
},
"autoload": {
"psr-4": {
"Predmond\\Calculator\\": "src/"
}
}
}

Before we forget, let’s dump the autoloader so our namespace can be used later:

$ composer dump-autoload
Generating autoload files

You should be able to run the phpspec CLI now:

# I like to have /vendor/bin in my path
export PATH="./vendor/bin:$PATH"
 
# Or run it with the path
$ ./vendor/bin/phpspec
phpspec 4.0.3
 
Usage:
command [options] [arguments]
...

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:

suites:
default:
namespace: Predmond\Calculator
psr4_prefix: Predmond\Calculator
formatter.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:

  1. Describe a Specification
  2. Run the Specification (Create the class if it doesn’t exist)
  3. Write an expected behavior
  4. Run the specification (create the method if it doesn’t exist)
  5. Write the implementation
  6. Verify the behavior
  7. 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:

$ ./vendor/bin/phpspec describe Predmond/Calculator/Calculator
 
# Or you can use backslashes with quotes
$ ./vendor/bin/phpspec describe "Predmond\Calculator\Calculator"

Running this command creates our specification file in spec/CalculatorSpec.php which looks like the following:

<?php
 
namespace spec\Predmond\Calculator;
 
use Predmond\Calculator\Calculator;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
 
class CalculatorSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Calculator::class);
}
}

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:

$ ./vendor/bin/phpspec run
Predmond\Calculator\Calculator
 
11 ! is initializable
class Predmond\Calculator\Calculator does not exist.
...
1 specs
1 examples (1 broken)
18ms
 
Do 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:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
 
1 specs
1 examples (1 passed)
9ms

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:

function it_can_add_two_numbers()
{
 
}

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:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 - can add two numbers
todo: write pending example
 
 
1 specs
2 examples (1 passed, 1 pending)
11ms

PhpSpec is telling us that we have a pending specification. Let’s drive it out now:

function it_can_add_two_numbers()
{
$this->add(2,3)->shouldReturn(5);
}

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:

# Other test suites
$calculator = new Calculator();
 
# PhpSpec $this is the Calculator
$this->add(); // And string some matching expectations...

If we try to run our specification now, PhpSpec will tell us the next step:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 ! can add two numbers
method Predmond\Calculator\Calculator::add not found.
 
---- broken examples
 
Predmond/Calculator/Calculator
16 ! can add two numbers
method Predmond\Calculator\Calculator::add not found.
 
 
1 specs
2 examples (1 passed, 1 broken)
11ms
 
Do you want me to create
`Predmond\Calculator\Calculator::add()` for you? [Y/n]
$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 ✘ can add two numbers
expected [integer:5], but got null.
 
---- failed examples
 
Predmond/Calculator/Calculator
16 ✘ can add two numbers
expected [integer:5], but got null.
 
 
1 specs
2 examples (1 passed, 1 failed)
11ms

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:

<?php
 
namespace Predmond\Calculator;
 
class Calculator
{
public function add($argument1, $argument2)
{
// TODO: write logic here
}
}

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:

public function add($argument1, $argument2)
{
return 5;
}

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:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 ✔ can add two numbers
 
 
1 specs
2 examples (2 passed)
20ms

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:

function it_can_add_two_numbers()
{
$this->add(2, 3)->shouldReturn(5);
$this->add(10, 0)->shouldReturn(10);
$this->add(0, 0)->shouldReturn(0);
$this->add(1, -2)->shouldReturn(-1);
}

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:

public function add($argument1, $argument2)
{
return $argument1 + $argument2;
}

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:

function it_can_divide_two_numbers()
{
$this->divide(10, 2)->shouldReturn(5);
}
 
function it_throws_a_division_by_zero_excption()
{
$this->shouldThrow('\Predmond\Calculator\DivisionByZeroException')->duringDivide(10, 0);
}

And the implementation:

public function divide($argument1, $argument2)
{
return $argument1 / $argument2;
}

If you run the specs you should get a division by zero warning:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 ✔ can add two numbers
24 ✔ can divide two numbers
29 ✘ throws a division by zero exception
expected exception of class "\Predmond\Calculator\Divi...", but got
[exc:PhpSpec\Exception\Example\ErrorException("warning: Division by zero in
/Users/predmond/Code/sandbox/calculator-phpspec/src/Calculator.php line 14")].

Let’s fix that by throwing the exception when the denominator is zero:

public function divide($numerator, $denominator)
{
if ($denominator === 0) {
throw new DivisionByZeroException();
}
 
return $numerator / $denominator;
}

And make sure to create the exception class in the src/ folder:

<?php
 
namespace Predmond\Calculator;
 
class DivisionByZeroException extends \Exception {}

And running our specs should pass now:

$ ./vendor/bin/phpspec run
 
Predmond\Calculator\Calculator
 
11 ✔ is initializable
16 ✔ can add two numbers
24 ✔ can divide two numbers
29 ✔ throws a division by zero exception
 
 
1 specs
4 examples (4 passed)
13ms

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:

$this->calculatorMemory()->shouldBe(5);
$this->getTitle()->shouldBeEqualTo("Star Wars");
$this->getReleaseDate()->shouldReturn(233366400);
$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.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article