Testing & TDD

Behavior-Driven Development (BDD)

20 min Lesson 28 of 35

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:
  1. Write a feature file for a user registration flow
  2. Include scenarios for: successful registration, duplicate email, invalid data
  3. Implement step definitions using Behat
  4. Add a scenario outline for password validation
  5. 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.