Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Behavior-Driven Development (BDD)

Overview

Behavior-Driven Development (BDD) là một agile methodology kết hợp TDD với principles của Domain-Driven Design. BDD tập trung vào behavior của hệ thống từ góc nhìn của stakeholders, sử dụng ngôn ngữ tự nhiên (Gherkin) để mô tả requirements.

Core Principles

1. Discovery

Làm việc với stakeholders để discover behaviors cần thiết.

2. Formulation

Chuyển behaviors thành executable specifications.

3. Automation

Automate specifications để drive development.

Gherkin Syntax

Feature: User Login
  As a registered user
  I want to log in to the system
  So that I can access my personalized content

  Scenario: Successful login
    Given I am on the login page
    And I have a valid account
    When I enter correct credentials
    Then I should be redirected to the dashboard
    And I should see a welcome message

  Scenario: Invalid credentials
    Given I am on the login page
    When I enter incorrect credentials
    Then I should see an error message
    And I should remain on the login page

  Scenario: Locked account after 3 failed attempts
    Given my account is locked
    When I try to log in
    Then I should see an account locked message
    And I should be redirected to the support page

Implementation with SpecFlow

1. Project Setup

<!-- .csproj -->
<PackageReference Include="SpecFlow" Version="3.9.0" />
<PackageReference Include="SpecFlow.xUnit" Version="3.9.0" />
<PackageReference Include="FluentAssertions" Version="6.0.0" />

2. Step Definitions

// Steps/LoginSteps.cs
public class LoginSteps
{
    private readonly LoginPage _loginPage;
    private readonly DashboardPage _dashboardPage;
    private string _errorMessage;
    
    public LoginSteps(LoginPage loginPage, DashboardPage dashboardPage)
    {
        _loginPage = loginPage;
        _dashboardPage = dashboardPage;
    }
    
    [Given(@"I am on the login page")]
    public void GivenIAmOnTheLoginPage()
    {
        _loginPage.NavigateTo();
    }
    
    [Given(@"I have a valid account")]
    public void GivenIHaveAValidAccount()
    {
        // Setup test data or mock
    }
    
    [When(@"I enter correct credentials")]
    public void WhenIEnterCorrectCredentials()
    {
        _loginPage.EnterUsername("testuser");
        _loginPage.EnterPassword("correctpassword");
        _loginPage.ClickLogin();
    }
    
    [Then(@"I should be redirected to the dashboard")]
    public void ThenIShouldBeRedirectedToTheDashboard()
    {
        _dashboardPage.IsDisplayed().Should().BeTrue();
    }
    
    [Then(@"I should see a welcome message")]
    public void ThenIShouldSeeAWelcomeMessage()
    {
        _dashboardPage.GetWelcomeMessage()
            .Should().Contain("Welcome");
    }
}

3. Page Objects

// Pages/LoginPage.cs
public class LoginPage
{
    private readonly IWebDriver _driver;
    
    public LoginPage(IWebDriver driver)
    {
        _driver = driver;
    }
    
    private IWebElement UsernameField => _driver.FindElement(By.Id("username"));
    private IWebElement PasswordField => _driver.FindElement(By.Id("password"));
    private IWebElement LoginButton => _driver.FindElement(By.CssSelector("button[type='submit']"));
    private IWebElement ErrorMessage => _driver.FindElement(By.CssSelector(".error-message"));
    
    public void NavigateTo() => _driver.Navigate().GoToUrl("https://app.example.com/login");
    
    public void EnterUsername(string username) => UsernameField.SendKeys(username);
    public void EnterPassword(string password) => PasswordField.SendKeys(password);
    public void ClickLogin() => LoginButton.Click();
    
    public string GetErrorMessage() => ErrorMessage.Text;
}

4. Hooks and Context

// Hooks/SetupHooks.cs
[Binding]
public class SetupHooks
{
    private readonly ScenarioContext _scenarioContext;
    
    public SetupHooks(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
    }
    
    [BeforeScenario]
    public void BeforeScenario()
    {
        // Setup test database
        TestDatabase.Reset();
        
        // Initialize web driver
        var options = new ChromeOptions();
        options.AddArgument("--headless");
        
        var driver = new ChromeDriver(options);
        _scenarioContext["Driver"] = driver;
    }
    
    [AfterScenario]
    public void AfterScenario()
    {
        // Cleanup
        var driver = _scenarioContext.Get<IWebDriver>("Driver");
        driver?.Quit();
    }
    
    [AfterStep]
    public void AfterStep()
    {
        // Take screenshot on failure
        if (_scenarioContext.TestError != null)
        {
            var driver = _scenarioContext.Get<IWebDriver>("Driver");
            ((ITakesScreenshot)driver).GetScreenshot()
                .SaveAsFile($"screenshots/{Guid.NewGuid()}.png");
        }
    }
}

5. Custom Transform

// Transform steps
[Given(@"I have a valid account")]
public void GivenIHaveAValidAccount()
{
    var user = new User
    {
        Username = "testuser",
        Password = HashPassword("correctpassword"),
        Status = UserStatus.Active
    };
    
    UserRepository.Add(user);
    _scenarioContext.Set(user);
}

// Table transformation
[Given(@"I have the following products in my cart:")]
public void GivenIHaveTheFollowingProductsInMyCart(Table table)
{
    var products = table.CreateSet<CartProduct>();
    foreach (var product in products)
    {
        ShoppingCart.Add(product);
    }
}

BDD in API Testing

// API Testing with BDD
[Binding]
public class ApiSteps
{
    private readonly HttpClient _httpClient;
    private ApiResponse _lastResponse;
    
    [Given(@"I am an authenticated user")]
    public void GivenIAmAnAuthenticatedUser()
    {
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", "test-token");
    }
    
    [When(@"I send a GET request to (.*)")]
    public async Task WhenISendAGETRequestTo(string endpoint)
    {
        _lastResponse = await _httpClient.GetAsync(endpoint);
    }
    
    [Then(@"the response status should be (.*)")]
    public void ThenTheResponseStatusShouldBe(HttpStatusCode statusCode)
    {
        _lastResponse.StatusCode.Should().Be(statusCode);
    }
    
    [Then(@"the response should contain:")]
    public void ThenTheResponseShouldContain(Table table)
    {
        var content = JsonSerializer.Deserialize<Dictionary<string, object>>(
            _lastResponse.Content);
            
        foreach (var row in table.Rows)
        {
            var key = row["Field"];
            var expectedValue = row["Value"];
            
            content.Should().ContainKey(key);
            content[key].ToString().Should().Be(expectedValue);
        }
    }
}

Feature File Organization

# Features/Orders/OrderPlacement.feature
Feature: Order Placement
  In order to buy products
  As a customer
  I want to place orders

  Background:
    Given the store has the following products:
      | Product    | Price | Stock |
      | Laptop     | 1000  | 10    |
      | Mouse      | 20    | 50    |
      | Keyboard   | 80    | 30    |
      
  @happy_path
  Scenario: Place an order with available items
    Given my cart contains:
      | Product | Quantity |
      | Laptop  | 1        |
    When I proceed to checkout
    And I complete the payment
    Then my order should be confirmed
    And the inventory should be reduced by 1 for "Laptop"

  @edge_case
  Scenario: Order with out-of-stock item
    Given my cart contains:
      | Product | Quantity |
      | Laptop  | 15       |
    When I proceed to checkout
    Then I should see an "insufficient stock" error
    And my cart should remain unchanged

Benefits

BenefitDescription
Clear CommunicationBusiness language in specs
Living DocumentationTests are always up-to-date
Shared UnderstandingCommon language for all team members
Focus on ValueTests describe business value
Early DetectionCatch issues early

Best Practices

  1. One Scenario per Behavior: Each test should cover one behavior
  2. Use Meaningful Names: Scenario names should describe the behavior
  3. Keep Steps Simple: Reuse steps across scenarios
  4. Automate Everything: Run BDD tests in CI/CD
  5. Review Together: Team review of feature files

Tools Comparison

ToolLanguageUse Case
SpecFlowC#.NET applications
CucumberJava/RubyMulti-language
BehavePythonPython applications
JasmineJavaScriptJavaScript apps

References