BDD FOR PHPUNIT
Gherkin-style specs that run inside PHPUnit.
php-conform reads Gherkin-style .suite specs and binds every Given/When/Then line to a step method in the PHP you already write. PHPUnit runs the scenarios, marks each step, and reports them like any other test — one runner, one config, nothing new wired into CI.
#Story: Member login As a site member I would like to show that valid credentials are accepted. #Scenario: Valid credentials are accepted Given a sign-in form When I enter [email protected] plus a valid password Then the member should be signed in
WHY PHP-CONFORM
You already have a test runner.
Behat is a fine framework — it is also a second framework. Its own binary, its own configuration, its own context layer, executed beside the PHPUnit suite your tooling already understands. php-conform takes the other door: your spec files become tests that PHPUnit discovers and runs. Same vendor/bin/phpunit. Same phpunit.xml. Same CI job, same IDE, same assertions — with scenarios your whole team can read.
| php-conform | A standalone BDD runner | |
|---|---|---|
| Test runner | PHPUnit — the one you run today | Its own binary and lifecycle |
| Configuration | Your existing phpunit.xml | A second config file to keep in sync |
| Step code | Step classes with docblock annotations | A separate context layer outside PHPUnit |
| Assertions | PHPUnit assertions, where they already live | Re-learned or bridged |
| CI | The PHPUnit job you already have | An additional pipeline step |
HOW IT MAPS
Every step line is a step method.
A .suite file is plain text: #Story: and #Scenario: headers, then Given/When/Then lines. Execution lives in step classes extending StepAbstract, with a docblock @given, @when, or @then annotation saying which sentences a method answers. Put $arg in the sentence and the matching word arrives as a method parameter. Need a pattern instead of a word? @pregmatch with a delimiter character turns the sentence into a regular expression — capture groups arrive in $matches. Your test class extends ScenarioSuite and pulls the step classes in — spec on one side, code on the other, one visible seam.
Given a sign-in form/** * @given a sign-in form */ public function createSignInForm() { $this->form = new SignInForm(); }
When I enter [email protected] plus a valid password
/** * @when I enter $arg1 plus $arg2 */ public function enterCredentials($arg1, $arg2) { $this->form->setEmail($arg1); $this->form->setPassword($arg2); }
When the login attempt is repeated 3 times/** * @pregmatch # * @when the login attempt is repeated (.*) times */ public function repeatLoginAttempt($matches) { $this->attempts = intval($matches[1]); }
Then the member should be signed in/** * @then the member should be signed in */ public function assertSignedIn() { $this->assertTrue($this->session->isSignedIn()); }
Same number, same color, same step.
DATA-DRIVEN
One scenario, every dataset.
Append an Examples: table to a scenario and give it <placeholder> columns. php-conform substitutes each row and runs the scenario once per dataset, marking the results for multi-dataset runs. The spec stays the width of a paragraph; the coverage multiplies.
#Story: Example Test As a software developer I would like to show that php-conform Can Process an example Suite. #Scenario: Example of a Data Driven BDD Test Given a calculator When I add <input_1> plus <input_2> Then the total should be <output> Examples: | input_1 | input_2 | output | | 20 | 30 | 50 | | 2 | 6 | 8 | | 10 | 40 | 50 |
********************************************************
** Story: Example of a Data Driven BDD Test (Multi) **
Steps:
[M] Given a calculator
[M] When I add <input_1> plus <input_2>
[M] Then the total should be <output>
DataSets:
|input_1|input_2|output|
0| 20 | 30 | 50 |
1| 2 | 6 | 8 |
2| 10 | 40 | 50 |
Scenario Details:
Total Asserts:6RESULTS
Output that names the failing line.
php-conform prints a story block for every scenario as it finishes, so a failing scenario fails specifically: every step gets its mark — [+] passed, [-] failed, [P] pending, [S] skipped, [M] multi-dataset, [A] not run — and the step that broke is the line you read, in the runner your CI already parses.
********************************************************
** Story: Invalid credentials are rejected (Failed) **
Steps:
[+] Given a sign-in form
[+] When I enter [email protected] plus a bad password
[-] Then an error should be shown
Failed: Failed asserting that false is true.
[A] And the lockout counter should increment
Scenario Details:
Total Asserts:1
**************************************************
** Story: Selenium tests (Skipped) **
Steps:
[S] Given a selenium test
Skipped: Selenium support is not installed/configured
[A] When a user navigates to google
[A] Then the active session page should be google
Scenario Details:
Total Asserts:0- [+]passed
- the step ran and its assertions held
- [-]failed
- the step ran and an assertion broke
- [P]pending
- no step method answers this line yet
- [S]skipped
- the step skipped itself (e.g. markTestSkipped)
- [M]multi-dataset
- the step ran once per Examples row
- [A]not run
- a skip or failure earlier in the scenario stopped it
GET STARTED
First scenario in four moves.
Install.
composer require --dev noizu-labs/php-conformRegister the extension.
phpunit.xml<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit-cache"> <testsuites> <testsuite name="default"> <directory>tests</directory> </testsuite> </testsuites> <extensions> <bootstrap class="\NoizuLabs\PHPConform\PHPUnitExtension\BDDExtension"/> </extensions> </phpunit>
The
<extensions>entry is the story output: a PHPUnit event extension, and without it the scenarios still run — you just see none of the story blocks. The<testsuites>entry is the other half: it tells PHPUnit where your tests live. Without one, PHPUnit runs nothing at all.Describe.
calculator_scenario.suite#Story: Smoke test As a developer I would like to show that the suite runs. #Scenario: The calculator adds Given a calculator When I add 2 plus 6 Then the total should be 8
Name the file after your test class — trailing
Testdropped, snake_cased, next to it in the same directory:CalculatorScenarioTest→calculator_scenario.suite.Wire and run.
CalculatorSteps.php<?php use NoizuLabs\PHPConform\PHPUnitExtension\StepAbstract; class CalculatorSteps extends StepAbstract { protected int $total = 0; /** @given a calculator */ public function createCalculator() { $this->total = 0; } /** @when I add $a plus $b */ public function addition($a, $b) { $this->total = (int) $a + (int) $b; } /** @then the total should be $total */ public function checkTotal($total) { $this->assertEquals((int) $total, $this->total); } }
PHPUnit only loads
*Test.phpfiles itself, so the step class has to come off composer's autoloader. The simplest way is a classmap over your tests directory — add this tocomposer.json, then runcomposer dump-autoload:composer.json"autoload-dev": { "classmap": ["tests/"] }
CalculatorScenarioTest.php<?php use NoizuLabs\PHPConform\PHPUnitExtension\ScenarioSuite; /** * @stepClass CalculatorSteps */ class CalculatorScenarioTest extends ScenarioSuite { }
$ vendor/bin/phpunit --no-progress
That is the whole ceremony: a spec file, a step class, a suite PHPUnit already knows how to run, and one extension entry that turns results into stories.