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
| Benefit | Description |
|---|---|
| Clear Communication | Business language in specs |
| Living Documentation | Tests are always up-to-date |
| Shared Understanding | Common language for all team members |
| Focus on Value | Tests describe business value |
| Early Detection | Catch issues early |
Best Practices
- One Scenario per Behavior: Each test should cover one behavior
- Use Meaningful Names: Scenario names should describe the behavior
- Keep Steps Simple: Reuse steps across scenarios
- Automate Everything: Run BDD tests in CI/CD
- Review Together: Team review of feature files
Tools Comparison
| Tool | Language | Use Case |
|---|---|---|
| SpecFlow | C# | .NET applications |
| Cucumber | Java/Ruby | Multi-language |
| Behave | Python | Python applications |
| Jasmine | JavaScript | JavaScript apps |