Acceptance Testing (Behat)
Acceptance tests verify end-to-end user scenarios using Behat with Gherkin syntax. Tests run against all repository implementations.
Gherkin Syntax
Feature: Library, book creation
Scenario: Create a book via API
When I create a book via API with title "Title" and author "Author" and ISBN "9781566199094"
Then I should get a 201 status code
And the book should be created
Keywords
Feature:- Describes the feature being testedScenario:- Single test caseGiven- Preconditions (setup)When- Action being testedThen- Expected outcomeAnd/But- Continue previous keyword
Step Definitions
#[When('I create a book via API with title ":title" and author ":author" and ISBN ":isbn"')]
public function whenICreateABookViaApi(string $title, string $author, string $isbn): void
{
$controller = self::$container->get(CreateBookControllerApi::class);
$request = new Request('POST', '/');
$request = $request->withParsedBody(['title' => $title, 'author' => $author, 'isbn' => $isbn]);
$this->response = $controller->index($request, new Response());
}
#[Then('I should get a :statusCode status code')]
public function thenIShouldGetStatusCode(int $statusCode): void
{
Assert::that($this->response->getStatusCode())->same($statusCode);
}
Data Tables
Given the following list of books:
| ID | Title | Author | ISBN |
| 1 | Clean Code | Robert Martin | 9780132350884 |
Running Tests
task tests:acceptance # All implementations
task tests:acceptance:demoInMemory # InMemory (fastest)
task tests:acceptance:demoSqlite # SQLite
task tests:acceptance:demoMysql # MySQL
Support Traits
BrowsePageTrait
Simulates HTTP requests and authentication:
trait BrowsePageTrait
{
#[Given('/I am authenticated as (.+)/')]
public function givenIAmAuthenticatedAs(string $email): void
{
$this->user = $userRepository->findByEmail(Email::fromString($email));
$sessionService->setUserAuthenticated($this->user->getId(), $this->user->getEmail());
}
#[When('/I browse the (.*) page/')]
public function whenIBrowseThePage(string $page): void
{
$urls = [
'Home' => ['/', HomeController::class, 'index'],
'List Books' => ['/books', ListBooksController::class, 'index'],
// ...
];
// Processes request through middleware and controller
}
}
GetContainerTrait
Initializes the DI container with the correct database configuration:
trait GetContainerTrait
{
protected static function getDiContainer(string $repositoryImplementation): Container
{
$_ENV['database.type'] = $repositoryImplementation;
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions(ROOT_DIR.'/config/demo/container.php');
$containerBuilder->addDefinitions(ROOT_DIR.'/config/demo/container_test.php');
self::$container = $containerBuilder->build();
// Register event subscriptions
$eventBus = self::$container->get(EventBusInterface::class);
// ...
}
}
Page Name Mapping
The BrowsePageTrait maps readable page names to URLs and controllers:
| Page Name | URL | Controller |
|---|---|---|
Home | / | HomeController::index |
List Books | /books | ListBooksController::index |
Detail Book | /books/{id} | DetailBookController::index |
Create new Book | /books/create | CreateBookController::showCreateForm |
List Loans | /loans | ListLoansController::index |
My Loans | /loans/my | MyLoansController::index |
Usage in scenarios:
Scenario Hooks
BeforeScenario
Initializes fresh database per scenario based on envRepositoryType:
#[BeforeScenario]
public static function beforeScenario(): void
{
$_ENV['database.type'] = getenv('envRepositoryType') ?? 'Sqlite';
self::$dbName = match ($_ENV['database.type']) {
'Mysql' => PdoRegistry::initializeMysql(true),
'Postgresql' => PdoRegistry::initializePostgresql(true),
'Sqlite' => PdoRegistry::initializeSqlite(true),
'InMemory' => null,
};
self::initContainer();
}
AfterScenario
Cleans up database after each scenario:
#[AfterScenario]
public static function afterScenario(): void
{
if ($_ENV['database.type'] !== 'InMemory' && self::$dbName !== null) {
match ($_ENV['database.type']) {
'Mysql' => PdoRegistry::cleanupMysql(self::$dbName),
'Postgresql' => PdoRegistry::cleanupPostgresql(self::$dbName),
'Sqlite' => PdoRegistry::cleanupSqlite(),
};
}
self::$container = null;
}
Running with Different Databases
The envRepositoryType environment variable selects the database:
task tests:acceptance:demoInMemory # envRepositoryType=InMemory
task tests:acceptance:demoSqlite # envRepositoryType=Sqlite
task tests:acceptance:demoMysql # envRepositoryType=Mysql
task tests:acceptance:demoPostgresql # envRepositoryType=Postgresql
Writing Good Scenarios
Do:
- Write from user perspective
- Use domain language
- Focus on behavior, not implementation
- Keep scenarios independent
Don't:
- Include technical details (IDs, SQL)
- Test implementation internals
- Create scenario dependencies
See Also
- Repository Implementations - Tests run against all implementations
- Controllers - Behat invokes controllers
- Database Schemas - Database initialization
- Authentication - User authentication in scenarios