friends-of-behat/service-container-extension
Declare custom Symfony DI services in Behat without writing a full extension. Import XML/YAML/PHP service definition files via behat.yml so your contexts and helpers can be wired through the Behat service container.
Installation:
composer require friends-of-behat/service-container-extension --dev
Configure Behat:
Add the extension to behat.yml and specify service import paths:
# behat.yml
default:
extensions:
FriendsOfBehat\ServiceContainerExtension:
imports:
- "%paths.base%/features/bootstrap/config/services.yml"
Define a Service: Create a YAML file (or XML/PHP) to declare services:
# features/bootstrap/config/services.yml
services:
app.test_user_repository:
class: App\Repositories\TestUserRepository
arguments:
- "@database.connection.testing"
Inject into Context: Use the service in a Behat context:
use Behat\Behat\Context\Context;
use App\Repositories\TestUserRepository;
class FeatureContext implements Context {
private TestUserRepository $userRepository;
public function __construct(TestUserRepository $userRepository) {
$this->userRepository = $userRepository;
}
/**
* @Given I have a test user
*/
public function iHaveATestUser() {
$this->userRepository->createTestUser();
}
}
Run Tests: Execute Behat as usual:
./vendor/bin/behat
Mocking External APIs: Define a mock service to replace a real API client in tests:
# features/bootstrap/config/services.yml
services:
app.mock_payment_gateway:
class: App\Services\MockPaymentGateway
calls:
- [setExpectedResponse, ["success"]]
Inject into a context to test payment flows without hitting a real service:
class PaymentContext {
public function __construct(private MockPaymentGateway $gateway) {}
/**
* @When I process a payment
*/
public function iProcessAPayment() {
$this->gateway->charge(100);
}
}
Leverage familiar Laravel-like syntax for service definitions:
services:
app.test_service:
class: App\Services\TestService
arguments:
- "@database.connection.testing"
- "%env(TEST_API_KEY)%"
calls:
- [configure, ["test_mode"]]
<container>
<services>
<service id="app.mock_logger" class="App\Services\MockLogger">
<argument type="service" id="monolog.logger.test" />
</service>
</services>
</container>
$container->setDefinition('app.test_factory', new Definition(
App\Factories\TestUserFactory::class,
[new Reference('database.connection.testing')]
));
class MyContext {
public function __construct(private TestService $service) {}
}
class MyContext {
private ?TestService $service;
public function setTestService(TestService $service) {
$this->service = $service;
}
}
Configure in behat.yml:
extensions:
FriendsOfBehat\ServiceContainerExtension:
services:
app.my_context:
class: App\Context\MyContext
calls:
- [setTestService, ["@app.test_service"]]
Use Laravel’s environment variables or Behat’s parameters:
# features/bootstrap/config/services.yml
services:
app.test_db_connection:
class: Illuminate\Database\Connection
factory: ["db", "connection"]
arguments:
- "testing"
- "%env(DB_TESTING_CONNECTION)%"
Bridge Laravel’s container with Behat’s (caution: avoid conflicts):
# features/bootstrap/config/services.yml
services:
app.test_mailer:
class: App\Services\TestMailer
factory: ["app", "make"]
arguments:
- App\Services\TestMailer::class
Define a factory service and inject it into contexts:
# services.yml
services:
app.test_user_factory:
class: App\Factories\TestUserFactory
arguments:
- "@database.connection.testing"
class UserContext {
public function __construct(private TestUserFactory $factory) {}
/**
* @Given a test user exists
*/
public function aTestUserExists() {
$this->factory->create(['name' => 'Test User']);
}
}
Create a mock service for external APIs:
# services.yml
services:
app.mock_stripe:
class: App\Services\MockStripeClient
calls:
- [setTestMode, [true]]
- [setResponse, ["charge_success"]]
Inject into contexts to simulate API responses:
class PaymentContext {
public function __construct(private MockStripeClient $stripe) {}
/**
* @When I charge $amount
*/
public function iChargeAmount(float $amount) {
$this->stripe->charge($amount);
}
}
Centralize reusable utilities (e.g., data generators, assertions):
# services.yml
services:
app.test_assertions:
class: App\Services\TestAssertions
shared: false # Prototype scope for each test
class SharedContext {
public function __construct(private TestAssertions $assertions) {}
/**
* @Then the response should match schema
*/
public function theResponseShouldMatchSchema(string $schema) {
$this->assertions->assertJsonSchema($schema);
}
}
Leverage Laravel’s Config:
Store service paths in config/behat.php:
return [
'service_paths' => [
database_path('config/behat/services.yml'),
base_path('tests/behat/config/services.yml'),
],
];
Then reference in behat.yml:
extensions:
FriendsOfBehat\ServiceContainerExtension:
imports:
- "%behat.service_paths%"
Combine with Laravel Testing:
Use Laravel’s RefreshDatabase trait alongside Behat:
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Repositories\TestUserRepository;
class FeatureContext {
public function __construct(private TestUserRepository $users) {}
/**
* @BeforeScenario
*/
public function resetDatabase() {
// Laravel's RefreshDatabase will handle this
}
}
Type-Hinting in IDE:
Add service definitions to PHPStan’s services.php for autocompletion:
return [
'services' => [
App\Services\TestService::class => FriendsOfBehat\ServiceContainerExtension\ServiceContainerExtension::class,
],
];
CI/CD Optimization: Cache service definitions in CI to speed up test runs:
# .github/workflows/tests.yml
jobs:
test:
steps:
- uses: actions/cache@v3
with:
path: vendor/friends-of-behat/service-container-extension
key: ${{ runner.os }}-behat-services-${{ hashFiles('features/bootstrap/config/**') }}
Service Not Found Errors:
behat.yml.%paths.base%:
imports:
- "%paths.base%/features/bootstrap/config/services.yml"
./vendor/bin/behat --debug
Circular Dependencies:
setPublic(false) in PHP definitions or avoid circular references in YAML/XML.Singleton vs. Prototype Scope:
services:
app.test_service:
class: App\Services\TestService
shared: false # Prototype scope
Laravel Container Conflicts:
app. or behat. to avoid collisions:
services:
behat.test_service: # Avoids clash with Laravel
How can I help you explore Laravel packages today?