friends-of-behat/symfony-extension
Behat extension that integrates Symfony 6 and 7 with a Mink driver, letting you define contexts as autowired Symfony services and test your application without setting up a server.
If you're coming from Laravel, the mental model is: Behat = Pest/PHPUnit, SymfonyExtension = Laravel's $this->get() HTTP testing layer + service container access, Mink symfony driver = a built-in KernelBrowser instead of Dusk. No web server, no Selenium, no php artisan serve — the kernel is booted in-process.
Minimal steps:
composer require --dev \
behat/behat \
behat/mink \
behat/mink-extension \
behat/mink-browserkit-driver \
friends-of-behat/symfony-extension:^2.6
(Pin the minor deliberately — see Gotchas. The symfony Mink driver is not usable without a BrowserKit driver; the extension hard-fails with a safety check if it's missing.)
behat.yml at project root:
default:
suites:
default:
contexts:
- App\Tests\Behat\FeatureContext
extensions:
FriendsOfBehat\SymfonyExtension:
kernel:
class: App\Kernel
environment: test
debug: true
Behat\MinkExtension:
sessions:
default:
symfony: ~
Register the context as a regular Symfony service in config/services_test.yaml (this is the whole point — the context is resolved from the container, not new-ed by Behat):
services:
_defaults:
autowire: true
autoconfigure: true
App\Tests\Behat\:
resource: '../../tests/Behat/*'
#[Given('I visit the homepage')]
public function iVisitTheHomepage(): void
{
$this->mink->getSession()->visit('/');
}
Run: vendor/bin/behat.
Where to look first: DOCUMENTATION.md — specifically Installation, Usage, Mink integration, and the Configuration reference. The features/ directory in the package is its own executable contract; if you want to know whether a behaviour is supported in your version, read the scenarios there rather than the prose.
First use case that pays off immediately: a smoke test that boots the app, hits /, and asserts the response plus a database row created through your real domain services — the exact job Laravel's RefreshDatabase + $this->get() does, but written in Gherkin so product can read it.
Contexts as services — inject what you'd app() in Laravel. Repositories, entity managers, mailers, clocks, HttpClient mocks, feature flags: all constructor-injected and autowired.
final class RegistrationContext implements Context
{
public function __construct(
private readonly UserRepository $users,
private readonly Mink $mink,
) {}
}
Arrange through services, act through HTTP. Mirrors the Laravel habit of User::factory() for setup and $this->post() for the assertion target. It also keeps scenarios fast and independent of HTML.
Page objects for the UI layer. With friends-of-behat/page-object-extension installed, page objects are autowired into contexts the same way — treat them like Laravel's Tests\Browser\Pages.
Compose contexts instead of a god-context. Since v2.0.5 one context may inject another, so FeatureContext can hold DatabaseContext, AuthContext, EmailContext and delegate step definitions. Keep the autowired constructor of each small; use bind in services_test.yaml for scalar config (API endpoints, timeouts, fixture paths).
Transactional isolation per scenario via hooks, since the package does not manage your database:
#[BeforeScenario]
public function beginTransaction(): void { $this->em->beginTransaction(); }
#[AfterScenario]
public function rollBack(): void { $this->em->rollback(); $this->em->clear(); }
If you go this route, make sure your kernel isn't rebooting a separate container between scenarios (see Gotchas).
Feature files as living documentation per bounded context, one suite per area (admin, api, storefront) rather than one giant suite — suites are cheap to add and let you run behat --suite=api in watch loops, the equivalent of php artisan test --filter.
CI recipe: compile the container once (bin/console cache:warmup --env=test) then run vendor/bin/behat --format=progress --no-colors --stop-on-failure. It's noticeably faster than PHPUnit feature tests since there's no HTTP round-trip and no server process to boot.
^2.6), otherwise Composer resolves something that fails on install. Check composer show before filing bugs.behat/symfony2-extension alongside this. They conflict conceptually, and stale config keys (kernel.file vs kernel.path) from old blog posts will silently do nothing.debug: false the container is not rebuilt on config changes; clear var/cache/test (or run bin/console cache:clear --env=test) after touching services_test.yaml. This is the equivalent of Laravel's cached config manifest biting you.Kernel::getContainer() semantics. Use the test container so private services are reachable, and ensure the test framework config is active in config/packages/test/framework.yaml. Reaching into the container by service id is a code smell in a context — autowire instead; it's the whole reason the package exists.$minkParameters is not autoconfigured. Since v2.0.3 you cannot get it injected without a typehint; if a context needs Mink parameters, implement MinkAwareContext / use the MinkAware trait, or pass the values in via bind in services_test.yaml. Cleanest: inject Behat\Mink\Mink directly and ignore the parameters array.symfony driver is BrowserKit — links, forms, redirects, cookies. Anything needing JS requires a real Mink driver session (base_url to an external server), at which point you've lost the "no server setup" benefit.webmozart/assert, PHPUnit's Assert if it's installed, or hand-rolled throw new \RuntimeException. Laravel devs are often surprised there's no expectException equivalent out of the box.contexts list and registered as services — one without the other produces confusing "context not found" or "service not found" errors depending on which is missing.services_test.yaml. Same failure mode as Laravel's container throwing on unresolvable __construct typehints, but the messaging is worse.mailer collector, or install zenstruck/mailer-test and inject the mailbox into your context.behat -vvv --format=pretty --stop-on-failure, plus var/log/test.log when kernel.debug: true. dump() from Symfony VarDumper works and is far better than var_dump in step definitions.kernel.bootstrap for custom kernel construction (e.g. loading .env.test.local yourself, like a custom bootstrap/app.php), a custom driver class if you need to wrap the kernel browser, and the PageObjectExtension integration for page-object autowiring.How can I help you explore Laravel packages today?