Testing & TDD
Behavior-Driven Development (BDD)
Behavior-Driven Development (BDD)
Behavior-Driven Development (BDD) bridges the gap between technical and non-technical team members by using natural language to describe application behavior. BDD focuses on the behavior of the application from the user's perspective.
Understanding BDD
BDD Benefits:
- Better collaboration between developers, QA, and business stakeholders
- Living documentation of system behavior
- Clearer requirements and acceptance criteria
- Tests written in business language
- Focus on user value
Gherkin Syntax
Gherkin is the language used to write BDD scenarios. It uses Given-When-Then format:
# features/user_authentication.feature
Feature: User Authentication
As a user
I want to login to the system
So that I can access my account
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter "user@example.com" as email
And I enter "password123" as password
And I click the "Login" button
Then I should see "Welcome back"
And I should be on the dashboard page
Scenario: Failed login with invalid credentials
Given I am on the login page
When I enter "wrong@example.com" as email
And I enter "wrongpass" as password
And I click the "Login" button
Then I should see "Invalid credentials"
And I should remain on the login page
Scenario Outline: Login validation
Given I am on the login page
When I enter "" as email
And I enter "" as password
And I click the "Login" button
Then I should see ""
Examples:
| email | password | message |
| valid@example.com | password123 | Welcome back |
| invalid@test.com | wrong | Invalid credentials |
| user@example.com | | Password is required |
| | password123 | Email is required |
Behat for PHP
Behat is the most popular BDD framework for PHP:
// Install Behat
composer require --dev behat/behat
vendor/bin/behat --init
// features/bootstrap/FeatureContext.php
use Behat\Behat\Context\Context;
use Behat\Behat\Tester\Exception\PendingException;
use PHPUnit\Framework\Assert;
class FeatureContext implements Context
{
private $response;
private $email;
private $password;
/**
* @Given I am on the login page
*/
public function iAmOnTheLoginPage()
{
$this->response = $this->get('/login');
Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
* @When I enter :arg1 as email
*/
public function iEnterAsEmail($email)
{
$this->email = $email;
}
/**
* @When I enter :arg1 as password
*/
public function iEnterAsPassword($password)
{
$this->password = $password;
}
/**
* @When I click the :arg1 button
*/
public function iClickTheButton($button)
{
$this->response = $this->post('/login', [
'email' => $this->email,
'password' => $this->password
]);
}
/**
* @Then I should see :arg1
*/
public function iShouldSee($text)
{
Assert::assertStringContainsString(
$text,
$this->response->getContent()
);
}
/**
* @Then I should be on the dashboard page
*/
public function iShouldBeOnTheDashboardPage()
{
Assert::assertEquals('/dashboard', $this->response->headers->get('Location'));
}
}
Laravel and Behat Integration
// Install Laravel Behat extension
composer require --dev laracasts/behat-laravel-extension
// behat.yml
default:
extensions:
Laracasts\Behat:
env_path: .env.behat
suites:
default:
contexts:
- FeatureContext
- Laracasts\Behat\Context\DatabaseTransactions
- Laracasts\Behat\Context\Services\FilesystemManager
Complex BDD Scenarios
# features/shopping_cart.feature
Feature: Shopping Cart Management
As a customer
I want to manage items in my cart
So that I can purchase products
Background:
Given the following products exist:
| name | price | stock |
| Laptop | 999 | 10 |
| Mouse | 25 | 50 |
| Keyboard | 75 | 30 |
And I am logged in as "customer@example.com"
Scenario: Add product to cart
Given I am on the product page for "Laptop"
When I click "Add to Cart"
Then I should see "Product added to cart"
And my cart should contain 1 item
And the cart total should be "$999.00"
Scenario: Update product quantity in cart
Given I have the following items in my cart:
| product | quantity |
| Laptop | 1 |
| Mouse | 2 |
When I am on the cart page
And I change the quantity of "Mouse" to 5
And I click "Update Cart"
Then the cart should contain:
| product | quantity | subtotal |
| Laptop | 1 | $999.00 |
| Mouse | 5 | $125.00 |
And the cart total should be "$1,124.00"
Scenario: Remove product from cart
Given I have "Laptop" in my cart
When I am on the cart page
And I click "Remove" for "Laptop"
Then my cart should be empty
And I should see "Your cart is empty"
// Implementation of shopping cart steps
class ShoppingCartContext implements Context
{
use DatabaseTransactions;
private $user;
private $cart;
/**
* @Given the following products exist:
*/
public function theFollowingProductsExist(TableNode $table)
{
foreach ($table->getHash() as $row) {
Product::create([
'name' => $row['name'],
'price' => $row['price'],
'stock' => $row['stock']
]);
}
}
/**
* @Given I am logged in as :email
*/
public function iAmLoggedInAs($email)
{
$this->user = User::factory()->create(['email' => $email]);
$this->actingAs($this->user);
}
/**
* @Given I have the following items in my cart:
*/
public function iHaveTheFollowingItemsInMyCart(TableNode $table)
{
$this->cart = Cart::create(['user_id' => $this->user->id]);
foreach ($table->getHash() as $row) {
$product = Product::where('name', $row['product'])->first();
$this->cart->items()->create([
'product_id' => $product->id,
'quantity' => $row['quantity']
]);
}
}
/**
* @Then my cart should contain :count item(s)
*/
public function myCartShouldContainItem($count)
{
$cart = Cart::where('user_id', $this->user->id)->first();
Assert::assertEquals($count, $cart->items->sum('quantity'));
}
/**
* @Then the cart total should be :total
*/
public function theCartTotalShouldBe($total)
{
$cart = Cart::where('user_id', $this->user->id)->first();
$actualTotal = '$' . number_format($cart->total(), 2);
Assert::assertEquals($total, $actualTotal);
}
}
Data Tables and Examples
# Testing multiple scenarios efficiently
Scenario Outline: Product price calculation
Given a product costs
When I add to my cart
Then the subtotal should be
Examples:
| price | quantity | total |
| 10.00 | 1 | 10.00 |
| 10.00 | 5 | 50.00 |
| 25.50 | 3 | 76.50 |
| 99.99 | 2 | 199.98 |
BDD Best Practices:
- Write scenarios before implementation (outside-in)
- Use business language, not technical jargon
- Focus on behavior, not implementation
- Keep scenarios independent
- Use Background for common setup
- Reuse step definitions across features
Tags for Test Organization
# Using tags to organize and filter tests
@smoke @critical
Feature: User Registration
@javascript @slow
Scenario: Register with email verification
# Steps here
@wip
Scenario: Social media registration
# Steps here
# Run specific tagged tests
vendor/bin/behat --tags=smoke
vendor/bin/behat --tags=~@slow # Exclude slow tests
vendor/bin/behat --tags="@smoke&&@critical"
Common BDD Mistakes:
- Writing scenarios that are too technical
- Testing implementation instead of behavior
- Creating dependent scenarios
- Overusing Background (makes scenarios unclear)
- Not involving stakeholders in writing scenarios
Exercise:
- Write a feature file for a user registration flow
- Include scenarios for: successful registration, duplicate email, invalid data
- Implement step definitions using Behat
- Add a scenario outline for password validation
- Use tags to mark critical scenarios
BDD vs TDD
Key Differences:
- TDD: Focuses on unit tests, implementation-driven, technical language
- BDD: Focuses on behavior, business-driven, natural language
- Both: Can be used together - BDD for features, TDD for implementation
BDD creates executable specifications that serve as both documentation and automated tests, improving communication and reducing misunderstandings between team members.