phpunit/phpunit-story
phpunit/phpunit-story brings behavior-driven development to PHPUnit, letting you write given/when/then story scenarios as test cases. A small, legacy extension useful for BDD-style specs, though now largely superseded by modern tooling.
Read the health signals before you type composer require. This package is archived, last touched in the PHPUnit 3.7/4.0 era, sits at 36 stars, and carries a NOASSERTION license. Its composer.json pins phpunit/phpunit to ~3.7|~4.0, so on any modern stack a plain install will die in dependency resolution long before it reaches your code. Treat this as reference material for a pattern, not a package you adopt.
Minimal viable steps if you genuinely need to run it:
# Only inside a legacy container: PHP 5.6/7.0, PHPUnit 4.8
composer require --dev phpunit/phpunit-story:~1.0
First use case — the whole point of the package is that a test method reads as a nested narrative instead of arrange/act/assert blocks:
class BankAccountTest extends PHPUnit_Extensions_Story_TestCase
{
public function testWithdrawingReducesBalance()
{
$this->given('a bank account with balance', 100)
->when('I withdraw', 20)
->then('the balance should be', 80);
}
public function givenABankAccountWithBalance($amount)
{
$this->account = new Account($amount);
}
public function whenIWithdraw($amount)
{
$this->account->withdraw($amount);
}
public function thenTheBalanceShouldBe($amount)
{
$this->assertEquals($amount, $this->account->getBalance());
}
}
Where to look first in the repo: the TestCase subclass that defines given()/when()/then()/and(), and the Scenario/Step value objects it accumulates. Those three files are the entire design; read them before deciding whether to port or adopt.
One scenario per test method. The chain is the scenario, and given() returns $this so the when/then calls append steps onto the same scenario object rather than executing immediately. Everything is deferred until PHPUnit invokes the test, which is why step methods can share $this state set up in setUp().
and() continues the previous step type. Use it to avoid repeating a prefix, but be aware it is not a new verb — it simply inherits the type of the step before it. Prefer explicit when/then in anything a reader will scan in CI output.
Step definitions belong in a base class, not the leaf test. A realistic suite looks like:
abstract class AccountStoryTestCase extends PHPUnit_Extensions_Story_TestCase
{
protected $account;
public function givenAnAccountWithBalance($amount) { $this->account = new Account($amount); }
public function whenIDeposit($amount) { $this->account->deposit($amount); }
public function thenTheBalanceShouldBe($amount) { $this->assertEquals($amount, $this->account->getBalance()); }
}
class OverdraftTest extends AccountStoryTestCase
{
public function testOverdraftIsRejected()
{
$this->given('an account with balance', 10)
->when('I withdraw', 50)
->then('the balance should be', 10);
}
}
This is the closest thing the package has to reuse, and it directly mirrors the Gherkin step-definition model without the Behat runtime.
Data providers still work. Because steps are just method calls inside a normal test method, @dataProvider rows re-run the entire scenario. Keep the provider on the scenario, not on individual steps, or the narrative fragments.
Inject collaborators in the given step. Mocks created in setUp() and consumed inside given* steps keep the prose readable; putting createMock() calls inline in a when step destroys the whole readability argument for the package.
Scenario is usable standalone if you want the runner without the base class — instantiate it, push steps, and invoke the run method from a plain PHPUnit_Framework_TestCase. This is the seam to use if you are experimenting rather than committing.
Step text maps to a method name mechanically, and it is brittle. The step type is concatenated with the ucwords'd, space-stripped text: 'a bank account with balance' becomes givenABankAccountWithBalance. Punctuation is not stripped — an apostrophe, comma, hyphen, or slash in a step string yields a method name PHP cannot resolve. Keep step text to plain lowercase words and digits.
Typos fail at runtime, not at parse time. Nothing validates that thenTheBalanceShouldBe exists until the step fires. A missing method surfaces as an undefined-method error attributed to the test method, not the step, so the failure message is unhelpful. Grep the step string before assuming a logic bug.
and inherits silently. If you change a when to a given, every downstream and changes meaning with no error. Prefer explicit verbs in anything long-lived.
Assertion failures abort the remaining chain. A failing then throws out of the scenario, so later steps never run and never report. Split long scenarios rather than chaining five then steps and losing four of them on the first failure.
No PHP 8, no attributes, no modern assertion signatures. assertEquals($expected, $actual) semantics changed in PHPUnit 8/9 (string-to-number comparison, assertEquals vs assertEqualsCanonicalizing), so even a successful legacy install produces subtly different assertions than your main suite.
Composer conflict is the real blocker, not runtime skill. The phpunit/phpunit constraint cannot be satisfied alongside PHPUnit 9/10/11. Forking and loosening the constraint means you inherit maintenance of a package nobody has touched in a decade. Budget a port, not a workaround.
Debugging is plain PHPUnit. --debug, --filter, --stop-on-failure all work as normal since steps run inside a standard test method. Add temporary fwrite(STDERR, ...) or var_dump inside the step method — there is no story-level verbose mode to lean on.
Extension points are thin. Override the step-dispatch method in your base class to log each step's type and text, or to add timing. Override given/when/then to attach metadata. Everything else requires patching the package, which is why the sane exit is a small trait:
trait StorySteps
{
public function given(string $text, ...$args) { $this->runStep('given', $text, $args); return $this; }
// ... when/then/and, each camelizing $text and calling $this->$method(...$args)
}
Twenty lines gets you 90% of the value inside a PHPUnit 10/11 test case, with no archived dependency, no version pin, and no license ambiguity.
Check the license before shipping. NOASSERTION on an archived repo means the Composer metadata and the repository text disagree or are absent. If you copy the Scenario/Step classes rather than require the package, have someone confirm the terms first — the BSD-3-Clause header in old Sebastian Bergmann packages is normally present but this listing does not attest to it.
Recommendation: do not adopt. Read the Scenario/Step/TestCase trio once to understand the deferred-chain design, then either implement the trait above or move to Behat/Codeception for real Gherkin, or Pest's describe()/it() if you only want narrative-shaped test names.
How can I help you explore Laravel packages today?