behat/phpunit-assertions-extension
Behat extension that brings PHPUnit assertions into Behat step definitions, letting you use assertSame, assertTrue, and other familiar assertions in scenarios for clearer, more expressive test failures.
This is a thin bridge package, not a testing framework. It lets Behat step definitions use PHPUnit's static assertion API (PHPUnit\Framework\Assert) and reports assertion failures as normal failed Behat steps. For a Laravel codebase that already has PHPUnit installed, the useful part is PHPUnit's Assert; the extension is the Behat glue.
Check your current versions first:
composer show behat/behat phpunit/phpunit
vendor/bin/behat --version
vendor/bin/phpunit --version
Install and register it:
composer require --dev behat/phpunit-assertions-extension
# behat.yml
default:
extensions:
Behat\PHPUnit\Extension\PHPUnitAssertionsExtension: ~
A first, realistic use case is asserting a Laravel HTTP response inside a Behat context:
Feature: API health
Scenario: health endpoint returns JSON
When I request "/up"
Then the response status should be 200
And the JSON should contain key "status"
use Behat\Behat\Context\Context;
use PHPUnit\Framework\Assert;
final class ApiContext implements Context
{
private $response;
/** @When I request :path */
public function iRequest(string $path): void
{
$this->response = $this->get($path); // assumes MakesHttpRequests setup
}
/** @Then the response status should be :status */
public function responseStatusShouldBe(int $status): void
{
Assert::assertSame($status, $this->response->getStatusCode());
}
/** @Then the JSON should contain key :key */
public function jsonShouldContainKey(string $key): void
{
Assert::assertArrayHasKey($key, $this->response->json());
}
}
Run it with vendor/bin/behat features/api.feature. If the step fails, Behat prints PHPUnit's assertion message and marks the step failed.
Look first at the package README and its composer.json in vendor/behat/phpunit-assertions-extension/ to confirm supported Behat/PHPUnit ranges. Also test whether you actually need the extension: because PHPUnit is already autoloaded in most Laravel projects, PHPUnit\Framework\Assert often works directly in a Behat context. Keep the dependency only if the extension is doing something for your setup.
Use static Assert calls directly. The day-to-day API is ordinary PHPUnit:
Assert::assertSame('active', $user->status);
Assert::assertTrue($response->isSuccessful());
Assert::assertArrayHasKey('id', $json);
Assert::assertStringContainsString('Welcome', $content);
Assert::assertJsonStringEqualsJsonString($expectedJson, $actualJson);
Always pass the optional message argument. Behat's step text is the only context a reader gets, so make the assertion self-explanatory.
Wrap domain assertions in a trait or base context. This keeps step definitions readable and makes later removal of the package easy:
trait ApiAssertions
{
private function assertJsonPath(string $path, mixed $expected, array $json): void
{
Assert::assertSame(
$expected,
data_get($json, $path),
"Expected JSON path [$path] to match."
);
}
}
Then use ApiAssertions; in your feature context.
Prefer Laravel's TestResponse assertions when you have them. If your Behat context uses Laravel's MakesHttpRequests trait and exposes a TestResponse, $response->assertOk(), $response->assertJsonPath(), and $response->assertJsonStructure() are already fluent and Laravel-aware. Use PHPUnit Assert for plain values, database state, or cases where no response assertion exists.
Use scenario outlines for data-driven cases. Behat does not use PHPUnit data providers. Put the table in the feature and assert each row:
Scenario Outline: status labels
Given a user with status "<status>"
Then the label should be "<label>"
Examples:
| status | label |
| active | Active |
| blocked | Blocked |
Use hooks for setup, assertions for checks. Bootstrap Laravel, refresh the database, or seed data in @BeforeScenario; keep assertions in the @Then steps. This extension does not bootstrap Laravel, manage transactions, or reset application state.
Consider a small injected assertion helper if contexts are services. Static calls are simplest, but a DomainAssertions service can be injected into contexts registered through behat.yml. That is useful when you want custom failure messages or shared assertions across several contexts.
Treat maturity as a risk. The package has very low adoption and explicitly says it is not recommended for new projects. Pin the version, keep usage behind a trait or helper, and be ready to remove it. For a new Laravel suite, prefer Pest/PHPUnit, or call PHPUnit\Framework\Assert directly if it already works in your Behat setup.
Check PHPUnit compatibility before adopting. PHPUnit 10/11 changed or removed some assertion APIs, and Behat extensions often lag behind. If Composer conflicts, either use a compatible PHPUnit line (for example PHPUnit 9.6 with Behat 3.x) or drop the extension and use the static Assert class directly.
expectException() is not available in Behat steps. Use try/catch plus Assert::fail():
try {
$this->postJson('/api/orders', []);
Assert::fail('Expected a validation exception.');
} catch (ValidationException $e) {
Assert::assertArrayHasKey('email', $e->errors());
}
Do not catch \Throwable around assertions. You can swallow PHPUnit's AssertionFailedError and turn a failure into a pass. Catch only the specific domain exception you expect, then let assertion failures propagate.
A Behat context is not a PHPUnit TestCase. $this->assertSame() is not available unless you add it yourself. Use Assert::assertSame() or the response object's assertions. Extending PHPUnit\Framework\TestCase from a Behat context is a bad idea because the lifecycle and constructor expectations conflict.
Use verbose Behat output when debugging. PHPUnit's assertion diff can be truncated by formatters. Run vendor/bin/behat -vvv or switch to a more verbose formatter when you cannot see the expected/actual values.
Laravel database state is your responsibility. Behat does not automatically apply RefreshDatabase. Add hooks or a transaction wrapper; otherwise assertions will pass or fail based on leftover data.
Keep autoloading predictable. Ensure features/bootstrap/bootstrap.php requires vendor/autoload.php, and add any custom Behat namespace to composer.json followed by composer dump-autoload.
Prefer extension points over subclassing. Avoid extending PHPUnit\Framework\Assert; it is a static utility class and subclassing is brittle. Use a trait, helper service, or PHPUnit constraints (PHPUnit\Framework\Constraint\*) if you need custom matching logic.
Plan the removal path. Replacing Assert::assertSame($expected, $actual) with a small local helper or a native if ($expected !== $actual) { throw new RuntimeException(...); } is usually a one-file change. That makes this low-adoption package safe to trial and easy to drop.
How can I help you explore Laravel packages today?