Skip to content

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 tested
  • Scenario: - Single test case
  • Given - Preconditions (setup)
  • When - Action being tested
  • Then - Expected outcome
  • And / 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:

When I browse the List Books page
When I browse the Detail Book page for book ID 123

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