laracasts/behat-laravel-extension
A Behat extension that boots your Laravel application inside feature tests, giving step definitions access to the container, facades, database helpers, and configuration so you can write BDD-style acceptance tests.
This extension is a thin bootstrapper: it loads your Laravel application (container, config, facades, Eloquent) before Behat scenarios run, so step definitions can use app(), resolve services, and hit the framework directly. It does not provide Mink/browser drivers or HTTP assertion helpers — those are yours to write.
composer require --dev laracasts/behat-laravel-extension
Because the last release was 2018, expect to pin versions (behat/behat:^3.4, Laravel 5.x/6.x era). On newer Laravel the Dotenv loading in the extension may need patching — see Gotchas.
.env.behat:APP_ENV=behat
APP_DEBUG=true
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database/testing.sqlite
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
behat.yml at the project root. The extension's config keys are env_path and bootstrap_path:default:
autoload:
'': features/bootstrap
extensions:
Laracasts\Behat\ServiceContainer\BehatExtension:
env_path: .env.behat
bootstrap_path: bootstrap/app.php
suites:
default:
contexts:
- FeatureContext
vendor/laracasts/behat-laravel-extension/src — it is small and tells you exactly when the app is booted and what it injects. Then write features/bootstrap/FeatureContext.php:<?php
use Behat\Behat\Context\Context;
class FeatureContext implements Context
{
private $response;
/** @When I request :method :uri */
public function iRequest($method, $uri)
{
$kernel = app(Illuminate\Contracts\Http\Kernel::class);
$request = Illuminate\Http\Request::create($uri, $method);
$this->response = $kernel->handle($request);
}
/** @Then the response status should be :status */
public function theResponseStatusShouldBe($status)
{
PHPUnit\Framework\Assert::assertSame((int) $status, $this->response->getStatusCode());
}
}
First use case: a login flow — seed a user with a factory, dispatch POST /login through the HTTP kernel, assert the redirect and that Auth::check() is true. That exercises routing, middleware, validation, and Eloquent in one readable scenario, which is exactly what this extension buys you.
Reuse the booted container in steps. The main value is that every step can call app(Foo::class), use facades (DB, Auth, Artisan), and touch real Eloquent models. Keep step definitions thin and push domain setup into factories/seeders.
Wrap each scenario in a transaction. The extension does not reset the database. Add hooks in your context:
/** @BeforeScenario @database */
public function beginTransaction() { app('db')->beginTransaction(); }
/** @AfterScenario @database */
public function rollBack() { app('db')->rollBack(); }
Because the HTTP kernel runs in the same process, it shares the connection — so the rollback works as long as your code doesn't explicitly commit. Run php artisan migrate:fresh --env=behat once before the suite (in CI or a wrapper script), not in a static @BeforeSuite hook, since static hooks run before Behat initializes contexts.
Authenticate by state, not by UI. For scenarios that aren't about login, use Auth::login($user) or app('auth')->loginUsingId($id) in a @Given I am logged in as :email step; save the browser-level login for the one scenario that actually tests it.
Build a small request/response trait. Store the last response on the context, add steps for status, JSON path, redirect, and session assertions. This is the closest you get to Laravel's MakesHttpRequests without pulling in a TestCase; reusing that trait directly is awkward because it assumes PHPUnit lifecycle methods.
Tag and segment suites. Use @database, @auth, @slow tags so you can run behat --tags='~@slow' locally and the full set in CI. Keep browser-driven scenarios (if you add Mink) in a separate suite with its own base URL.
Call Artisan from steps when a scenario depends on a command (Artisan::call('passport:install')). This works because the console kernel is bootstrapped by the extension; it is much faster than shelling out.
Maintenance risk is the headline. Last release 2018; it targets Behat 3 and older Laravel. On Laravel 6+, the Dotenv API changed and the extension's manual env loading can fail. If it breaks, the extension point is trivial: replace it with your own @BeforeSuite/context constructor that does require 'vendor/autoload.php'; $app = require 'bootstrap/app.php'; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); and sets APP_ENV/env file yourself.
env_path is relative to the working directory, not the config file. Always run Behat from the project root, and never point it at your real .env — you will truncate a development database at some point.
Clear config cache before running. php artisan config:cache bakes env values into bootstrap/cache/config.php, so APP_ENV=behat is ignored and you talk to the wrong database. Add php artisan config:clear to your test script. Same for route cache if routes are environment-dependent.
Container state persists for the whole Behat process. Contexts are instantiated per scenario, but the application, resolved singletons, and static caches are not. If your code caches config or holds state in a singleton, reset it in an @AfterScenario hook, or accept that scenario two may see scenario one's leftovers.
In-memory SQLite is a trap if anything opens a second connection (queue workers in-process, parallel connections). Use a file-backed SQLite database or MySQL for the Behat env. For speed, keep migrations once-per-suite and transactions per scenario.
Debugging: behat -vvv --stop-on-failure, then var_dump(app()->environment()) in a step to confirm which env booted. Check storage/logs/laravel.log — exceptions thrown inside the HTTP kernel are logged there rather than always surfacing as Behat failures. If app() is undefined, the extension did not boot: verify the FQCN in behat.yml and that bootstrap_path exists relative to cwd.
Extension points: if you need explicit injection rather than the global helper, write a Behat\Behat\Context\Initializer\ContextInitializer that passes the application into contexts implementing a LaravelAwareContext interface, or simply grab app() in your base context's constructor and assert it booted. Both are a few lines and decouple you from the package's internals if you later replace it.
Alternatives worth weighing: for new projects, Laravel's HTTP testing, Pest, and Dusk cover this with active maintenance. Behat remains worthwhile when non-developers read the Gherkin or you already have a Behat suite — in that case, budget a day to vendor the bootstrapping logic if the extension doesn't install cleanly on your Laravel version.
How can I help you explore Laravel packages today?