php-conform

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.

login.suite
#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
[+] 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.

What changes when you add BDD to an existing PHPUnit suite.
php-conformA standalone BDD runner
Test runnerPHPUnit — the one you run todayIts own binary and lifecycle
ConfigurationYour existing phpunit.xmlA second config file to keep in sync
Step codeStep classes with docblock annotationsA separate context layer outside PHPUnit
AssertionsPHPUnit assertions, where they already liveRe-learned or bridged
CIThe PHPUnit job you already haveAn 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.

login.suite
LoginSteps.php
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.

calculator.suite
#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 output
********************************************************
** 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:6

RESULTS

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.

vendor/bin/phpunit --no-progressstory
********************************************************
** 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.

  1. Install.

    composer require --dev noizu-labs/php-conform
  2. Register 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.

  3. 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 Test dropped, snake_cased, next to it in the same directory: CalculatorScenarioTest → calculator_scenario.suite.

  4. 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.php files itself, so the step class has to come off composer's autoloader. The simplest way is a classmap over your tests directory — add this to composer.json, then run composer 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.