PhpSpec is a Behavior-Driven Development (BDD) framework for PHP. Unlike traditional unit testing frameworks like PHPUnit, which focus on testing the internal implementation details of code, PhpSpec emphasizes describing the *behavior* of your code from the perspective of its users or collaborators. This approach encourages thinking about the public API and intended interactions, leading to clearer, more maintainable code and serving as living documentation.
Key Concepts:
1. Specification: Instead of 'tests', PhpSpec uses 'specifications'. A specification is a class, typically named `[SubjectName]Spec.php` (e.g., `WalletSpec.php` for a `Wallet` class), that describes how a 'subject' (the class under specification) should behave.
2. Subject: The class or object whose behavior is being specified.
3. Examples: Methods within a specification that begin with `it_should...`, `it_is...`, or `it_can...` are called 'examples'. Each example describes a specific aspect of the subject's behavior. They read like plain language sentences, making the specification easy to understand.
4. Matchers: PhpSpec provides a rich set of matchers (e.g., `->shouldReturn()`, `->shouldBe()`, `->shouldThrow()`, `->shouldHaveType()`) to make assertions about the subject's behavior. These matchers are chained after the subject or its methods to express expectations.
5. Let Method (`let()`): This special method in a specification is used for setting up the subject or its dependencies before each example runs, similar to `setUp()` in PHPUnit, but often more focused on creating the subject.
Workflow:
PhpSpec promotes a red-green-refactor cycle:
1. Red: Write a failing specification describing a desired behavior for your class. When you run PhpSpec, it will tell you the class or method doesn't exist, or the behavior isn't implemented yet.
2. Green: Implement the minimum amount of code in your subject class to make the specification pass.
3. Refactor: Clean up your code, ensuring it remains readable and efficient, without changing its observable behavior (as verified by the passing specifications).
Benefits of Using PhpSpec:
* Clearer Communication: Specifications serve as highly readable documentation of your system's behavior, understandable by both developers and non-technical stakeholders.
* Focus on Behavior: Encourages designing public interfaces based on how they will be used, rather than getting bogged down in implementation details.
* Living Documentation: Your specifications are always up-to-date with the actual behavior of your code, unlike separate documentation that can quickly become stale.
* Better Design: Promotes writing loosely coupled code and thinking about responsibilities, as specifications interact with the subject purely through its public API.
Example Code
```php
<?php
// src/Wallet.php
// This is the class we will specify.
namespace App;
class Wallet
{
private int $balance;
public function __construct(int $initialBalance = 0)
{
if ($initialBalance < 0) {
throw new \InvalidArgumentException('Initial balance cannot be negative.');
}
$this->balance = $initialBalance;
}
public function getBalance(): int
{
return $this->balance;
}
public function deposit(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('Deposit amount must be positive.');
}
$this->balance += $amount;
}
public function withdraw(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('Withdrawal amount must be positive.');
}
if ($this->balance < $amount) {
throw new \Exception('Insufficient funds.');
}
$this->balance -= $amount;
}
}
// spec/App/WalletSpec.php
// This is the PhpSpec specification for the Wallet class.
// To run this, you would typically have PhpSpec installed via Composer (composer require --dev phpspec/phpspec).
// Then, from your project root, run: vendor/bin/phpspec run
namespace spec\App;
use App\Wallet;
use PhpSpec\ObjectBehavior;
class WalletSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Wallet::class);
}
function it_has_a_zero_balance_by_default()
{
$this->getBalance()->shouldReturn(0);
}
function it_can_be_initialized_with_a_positive_balance()
{
$this->beConstructedWith(100); // Reconstructs the subject with an initial balance
$this->getBalance()->shouldReturn(100);
}
function it_throws_an_exception_when_initialized_with_a_negative_balance()
{
$this->beConstructedWith(-50); // Try to construct with invalid value
$this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation();
}
function it_deposits_money()
{
$this->deposit(50);
$this->getBalance()->shouldReturn(50);
}
function it_should_not_allow_depositing_negative_amounts()
{
$this->shouldThrow(\InvalidArgumentException::class)->during('deposit', [-10]);
}
function it_withdraws_money()
{
$this->beConstructedWith(100);
$this->withdraw(30);
$this->getBalance()->shouldReturn(70);
}
function it_should_not_withdraw_more_than_available()
{
$this->beConstructedWith(50);
$this->shouldThrow(\Exception::class)->during('withdraw', [100]);
}
function it_should_not_allow_withdrawing_negative_amounts()
{
$this->shouldThrow(\InvalidArgumentException::class)->during('withdraw', [-20]);
}
}
```








phpspec/phpspec