Sobre nós Guias Projetos Contactos
Админка
please wait

O Behat é uma framework PHP para Behavior-Driven Development (BDD) que utiliza a sintaxe Gherkin, legível por humanos, para descrever o comportamento da aplicação. Faz a ponte entre os requisitos de negócio e os testes automatizados, tornando as especificações executáveis. Este guia aborda a implementação de BDD de forma eficaz, na perspetiva de um programador sénior.

Porquê BDD com Behat

O Behat oferece vantagens significativas:

  1. Documentação Viva: Os testes servem como especificações atualizadas
  2. Comunicação com Stakeholders: Pessoas não técnicas podem ler e escrever cenários
  3. Testes orientados ao negócio: Foco no comportamento, não na implementação
  4. Testes de API e UI: Teste em qualquer camada da sua aplicação
  5. Integração com PHP: PHP nativo, funciona com Laravel, Symfony e outros

Instalação e Configuração

Instalar via Composer

composer require --dev behat/behat
composer require --dev guzzlehttp/guzzle # Para testes de API
composer require --dev behat/mink behat/mink-extension # Para testes no browser

Inicializar o Projeto

vendor/bin/behat --init

Isto cria:

  • features/ - Diretório para ficheiros de funcionalidades
  • features/bootstrap/ - Diretório para classes de contexto
  • features/bootstrap/FeatureContext.php - Contexto predefinido

Configuração

Crie behat.yml:

default:
suites:
default:
contexts:
- FeatureContext
- ApiContext
- WebContext
extensions:
Behat\MinkExtension:
base_url: http://localhost:8000
sessions:
default:
goutte: ~

Sintaxe Gherkin

Ficheiros de Funcionalidade

Crie features/user-registration.feature:

Feature: User Registration
As a visitor
I want to create an account
So that I can access member features
Background:
Given the system is running
And no user exists with email "[email protected]"
Scenario: Successful registration with valid data
Given I am on the registration page
When I fill in "name" with "John Doe"
And I fill in "email" with "[email protected]"
And I fill in "password" with "SecurePass123!"
And I fill in "password_confirmation" with "SecurePass123!"
And I press "Register"
Then I should see "Welcome, John Doe"
And I should be logged in
Scenario: Registration fails with invalid email
Given I am on the registration page
When I fill in "email" with "invalid-email"
And I press "Register"
Then I should see "Please enter a valid email address"
And I should not be logged in
Scenario Outline: Password validation
Given I am on the registration page
When I fill in "password" with "<password>"
And I press "Register"
Then I should see "<error_message>"
Examples:
| password | error_message |
| short | Password must be at least 8 chars |
| nodigits | Password must contain a number |
| 12345678 | Password must contain a letter |

Palavras-chave do Gherkin

  • Feature: Descreve a funcionalidade que está a ser testada
  • Background: Passos executados antes de cada cenário
  • Scenario: Um caso de teste específico
  • Scenario Outline: Modelo para testes orientados por dados
  • Examples: Tabela de dados para o Scenario Outline
  • Given: Pré-condições (preparação)
  • When: Ações (o que o utilizador faz)
  • Then: Asserções (resultados esperados)
  • And/But: Continuação do tipo de passo anterior

Implementar Definições de Passos

Classes de Contexto

Edite features/bootstrap/FeatureContext.php:

<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
class FeatureContext implements Context
{
private $response;
private $currentUser;
/**
* @Given o sistema está em execução
*/
public function theSystemIsRunning()
{
// Verificar que a aplicação está acessível
$client = new GuzzleHttp\Client();
$response = $client->get('http://localhost:8000/health');
if ($response->getStatusCode() !== 200) {
throw new Exception("System is not running");
}
}
/**
* @Given não existe nenhum utilizador com o e-mail :email
*/
public function noUserExistsWithEmail($email)
{
// Limpar dados de teste
$this->deleteUserByEmail($email);
}
/**
* @When eu preencho :field com :value
*/
public function iFillInWith($field, $value)
{
$this->formData[$field] = $value;
}
/**
* @Then eu devo ver :text
*/
public function iShouldSee($text)
{
if (strpos($this->response->getBody(), $text) === false) {
throw new Exception("Text '$text' not found in response");
}
}
/**
* @Then eu devo ter sessão iniciada
*/
public function iShouldBeLoggedIn()
{
if (!isset($this->currentUser)) {
throw new Exception("User is not logged in");
}
}
}

Contexto de Testes de API

Crie features/bootstrap/ApiContext.php:

<?php
use Behat\Behat\Context\Context;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class ApiContext implements Context
{
private Client $client;
private $response;
private array $headers = [];
private $requestBody;
public function __construct()
{
$this->client = new Client([
'base_uri' => 'http://localhost:8000',
'http_errors' => false,
]);
}
/**
* @Given eu sou um utilizador não autenticado
*/
public function iAmAnUnauthenticatedUser()
{
unset($this->headers['Authorization']);
}
/**
* @Given eu estou autenticado como :email
*/
public function iAmAuthenticatedAs($email)
{
// Iniciar sessão e obter token
$response = $this->client->post('/api/login', [
'json' => [
'email' => $email,
'password' => 'password',
],
]);
$data = json_decode($response->getBody(), true);
$this->headers['Authorization'] = 'Bearer ' . $data['token'];
}
/**
* @When eu envio um pedido :method para :endpoint
*/
public function iSendARequestTo($method, $endpoint)
{
$options = ['headers' => $this->headers];
if ($this->requestBody) {
$options['json'] = $this->requestBody;
}
$this->response = $this->client->request($method, $endpoint, $options);
$this->requestBody = null;
}
/**
* @When eu envio um pedido :method para :endpoint com body:
*/
public function iSendARequestToWithBody($method, $endpoint, PyStringNode $body)
{
$this->requestBody = json_decode($body->getRaw(), true);
$this->iSendARequestTo($method, $endpoint);
}
/**
* @Then o código de estado da resposta deve ser :code
*/
public function theResponseStatusCodeShouldBe($code)
{
$actual = $this->response->getStatusCode();
if ($actual !== (int) $code) {
throw new Exception("Expected status $code, got $actual");
}
}
/**
* @Then a resposta deve conter :text
*/
public function theResponseShouldContain($text)
{
$body = $this->response->getBody()->getContents();
if (strpos($body, $text) === false) {
throw new Exception("Response does not contain '$text'");
}
}
/**
* @Then a resposta deve ser JSON contendo:
*/
public function theResponseShouldBeJsonContaining(PyStringNode $expected)
{
$actual = json_decode($this->response->getBody(), true);
$expected = json_decode($expected->getRaw(), true);
foreach ($expected as $key => $value) {
if (!isset($actual[$key]) || $actual[$key] !== $value) {
throw new Exception("JSON mismatch for key '$key'");
}
}
}
/**
* @Then a resposta deve incluir um anúncio com o ID :id
*/
public function theResponseShouldIncludeAnAnnouncementWithId($id)
{
$data = json_decode($this->response->getBody(), true);
$announcements = $data['_embedded']['announcement'] ?? [];
foreach ($announcements as $announcement) {
if ($announcement['id'] == $id) {
return;
}
}
throw new Exception("Announcement with ID $id not found");
}
}

Exemplo de Funcionalidade de API

Crie features/api/announcements.feature:

Feature: Announcements API
As an API consumer
I want to manage announcements
So that I can display them to users
Scenario: List all announcements
Given I am an unauthenticated user
When I send a "GET" request to "/api/announcements"
Then the response status code should be 200
And the response should include an announcement with ID "12345"
Scenario: Create announcement requires authentication
Given I am an unauthenticated user
When I send a "POST" request to "/api/announcements" with body:
"""
{
"title": "New Feature",
"body": "We launched something amazing!"
}
"""
Then the response status code should be 401
Scenario: Authenticated user can create announcement
Given I am authenticated as "[email protected]"
When I send a "POST" request to "/api/announcements" with body:
"""
{
"title": "New Feature",
"body": "We launched something amazing!"
}
"""
Then the response status code should be 201
And the response should be JSON containing:
"""
{
"title": "New Feature"
}
"""

Testes Web com Mink

Contexto Web

Crie features/bootstrap/WebContext.php:

<?php
use Behat\MinkExtension\Context\MinkContext;
class WebContext extends MinkContext
{
/**
* @Given eu tenho sessão iniciada como :email
*/
public function iAmLoggedInAs($email)
{
$this->visit('/login');
$this->fillField('email', $email);
$this->fillField('password', 'password');
$this->pressButton('Login');
$this->assertPageContainsText('Dashboard');
}
/**
* @When eu clico no link :link na navegação
*/
public function iClickOnTheLinkInTheNavigation($link)
{
$this->getSession()
->getPage()
->find('css', 'nav')
->clickLink($link);
}
/**
* @Then o título da página deve ser :title
*/
public function thePageTitleShouldBe($title)
{
$actual = $this->getSession()
->getPage()
->find('css', 'title')
->getText();
if ($actual !== $title) {
throw new Exception("Expected title '$title', got '$actual'");
}
}
/**
* @Then eu devo ver :count itens na lista
*/
public function iShouldSeeItemsInTheList($count)
{
$items = $this->getSession()
->getPage()
->findAll('css', '.list-item');
if (count($items) !== (int) $count) {
throw new Exception("Expected $count items, found " . count($items));
}
}
}

Web Scraping com Goutte

<?php
use Behat\Behat\Context\Context;
use Goutte\Client;
class ScrapingContext implements Context
{
private Client $client;
private $crawler;
public function __construct()
{
$this->client = new Client();
}
/**
* @Given eu abro a página :url
*/
public function iOpenThePage($url)
{
$this->crawler = $this->client->request('GET', $url);
}
/**
* @When eu clico no link :text
*/
public function iClickTheLink($text)
{
$link = $this->crawler->selectLink($text)->link();
$this->crawler = $this->client->click($link);
}
/**
* @Then o cabeçalho da página deve ser :heading
*/
public function thePageHeadingShouldBe($heading)
{
$actual = trim($this->crawler->filter('h1')->first()->text());
if ($actual !== $heading) {
throw new Exception("Expected h1 '$heading', got '$actual'");
}
}
}

Executar Testes

Comandos Básicos

# Executar todas as funcionalidades
vendor/bin/behat
# Executar funcionalidade específica
vendor/bin/behat features/user-registration.feature
# Executar cenário específico pelo número da linha
vendor/bin/behat features/user-registration.feature:15
# Executar com tag específica
vendor/bin/behat --tags="@api"
# Dry run (verificar definições de passos sem executar)
vendor/bin/behat --dry-run
# Gerar definições de passos em falta
vendor/bin/behat --append-snippets

Utilizar Tags

Atribua tags aos cenários para organização:

@api @authentication
Feature: Authentication API
@smoke
Scenario: Valid login returns token
...
@slow
Scenario: Rate limiting after failed attempts
...

Execute cenários com tag:

vendor/bin/behat --tags="@api"
vendor/bin/behat --tags="@smoke"
vendor/bin/behat --tags="~@slow" # Excluir testes lentos

Integração com CI

GitHub Actions

name: BDD Tests
on: [push, pull_request]
jobs:
behat:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: testing
ports:
- 3306:3306
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pdo_mysql
- name: Install dependencies
run: composer install
- name: Start application
run: php artisan serve &
- name: Run Behat
run: vendor/bin/behat --format=progress

Principais Conclusões

  1. Escreva primeiro os cenários: Defina o comportamento antes da implementação
  2. Use linguagem do domínio: Os cenários devem ler-se como requisitos
  3. Mantenha os passos reutilizáveis: Construa uma biblioteca de definições de passos
  4. Separe responsabilidades: Use contextos diferentes para API, web e base de dados
  5. Aplique tags de forma estratégica: Organize os testes para execução seletiva
  6. Integre com CI: Execute testes BDD em cada commit

O Behat transforma especificações em testes executáveis, garantindo que a sua aplicação se comporta exatamente como os stakeholders esperam, ao mesmo tempo que cria documentação viva que se mantém atual.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive