Overview
Behavior-Driven Development (BDD) extends Test-Driven Development (TDD) by focusing on the behavior of an application from the end user's perspective. Rather than writing tests that verify implementation details, BDD emphasizes describing how software should behave in specific scenarios using domain language accessible to all project stakeholders.
BDD emerged from Dan North's work in 2003 while addressing recurring patterns in teaching TDD. Teams consistently struggled with knowing where to start testing, what to test, what not to test, and how to name their tests. BDD provides a structured approach to these questions by centering the conversation on behavior and business value.
The methodology operates at multiple levels. At the specification level, scenarios describe expected behaviors in plain language that business stakeholders can read and verify. At the implementation level, these scenarios become executable tests that verify the software matches the specified behavior. This dual nature makes BDD both a specification technique and a testing approach.
BDD introduces a ubiquitous language shared between developers, testers, business analysts, and domain experts. This language appears in both conversations and code, reducing translation errors and ensuring everyone discusses the same behaviors using consistent terminology.
Feature: User Authentication
As a registered user
I want to log into the system
So that I can access my personal dashboard
Scenario: Successful login with valid credentials
Given I am on the login page
And I have registered with email "user@example.com"
When I enter email "user@example.com"
And I enter password "secure_password"
And I click the login button
Then I should see my dashboard
And I should see a welcome message
This scenario demonstrates BDD's natural language format. Non-technical stakeholders can read and understand what the system should do, while the scenario also serves as an executable test specification.
Key Principles
BDD operates on three core principles that distinguish it from other development methodologies.
Outside-In Development drives the development process starting from the user's perspective rather than from technical implementation details. Teams begin by describing how users interact with the system, then build the components necessary to support those interactions. This approach ensures every piece of code serves a clear user need.
The process typically flows from acceptance tests describing complete user workflows down to unit tests verifying individual component behaviors. Each outer test drives the creation of inner components, forming a chain from user behavior to implementation detail.
Ubiquitous Language establishes shared vocabulary between all project stakeholders. Technical team members and business experts use identical terms when discussing features, reducing misunderstandings and translation overhead. This language appears in conversations, specifications, and code.
The domain language evolves throughout the project as the team's understanding deepens. Terms become more precise, ambiguities get resolved through discussion, and the vocabulary grows to accommodate new concepts. Code reflects this language directly through class names, method names, and variable names that match business terminology.
Three Amigos Collaboration brings together three perspectives during specification workshops: business perspective (what value this provides), development perspective (how to build it), and testing perspective (what could go wrong). This collaboration typically occurs before implementation begins.
The three amigos discuss concrete examples of how features should work in specific situations. These examples become the foundation for executable specifications. The conversation uncovers edge cases, clarifies requirements, and identifies missing information before coding starts.
BDD scenarios follow a specific structure that separates context, action, and outcome:
# RSpec example showing BDD principles
RSpec.describe 'Shopping Cart' do
describe 'adding items' do
context 'when the cart is empty' do
it 'increases the item count' do
cart = ShoppingCart.new
cart.add_item(Product.new(name: 'Book'))
expect(cart.item_count).to eq(1)
end
end
context 'when adding a duplicate item' do
it 'increases the quantity instead of adding a new line' do
cart = ShoppingCart.new
product = Product.new(name: 'Book')
cart.add_item(product)
cart.add_item(product)
expect(cart.line_items.count).to eq(1)
expect(cart.line_items.first.quantity).to eq(2)
end
end
end
end
Each specification describes a single behavior in a specific context. The test name reads as a sentence describing what happens, making the test suite serve as living documentation.
Discovery Through Examples uses concrete scenarios to explore and document system behavior. Abstract requirements become clear through specific examples that demonstrate how the system should respond to particular inputs.
Teams identify multiple examples for each feature, including typical cases, edge cases, and error conditions. These examples drive out hidden assumptions and reveal gaps in understanding. The conversation around examples often produces more value than the resulting specifications themselves.
Ruby Implementation
Ruby provides extensive tooling for BDD through frameworks that support both specification-style testing and natural language scenarios.
RSpec represents the dominant BDD framework in Ruby. It provides a domain-specific language for writing specifications that read like sentences describing behavior:
RSpec.describe Order do
describe '#total' do
context 'with standard items' do
it 'sums the item prices' do
order = Order.new
order.add_item(price: 10.00)
order.add_item(price: 15.00)
expect(order.total).to eq(25.00)
end
end
context 'with discounts applied' do
it 'subtracts the discount from the subtotal' do
order = Order.new
order.add_item(price: 100.00)
order.apply_discount(percentage: 10)
expect(order.total).to eq(90.00)
end
it 'does not apply discounts below the minimum order value' do
order = Order.new
order.add_item(price: 5.00)
order.apply_discount(percentage: 10, minimum: 20.00)
expect(order.total).to eq(5.00)
end
end
end
end
RSpec organizes specifications using describe and context blocks that establish the scenario, while it blocks state expected outcomes. This structure maps directly to BDD's emphasis on context and behavior.
The framework includes extensive matcher libraries for expressing expectations naturally:
# Collection matchers
expect(items).to include(target_item)
expect(cart.items).to all(be_a(Product))
expect(results).to contain_exactly(1, 2, 3)
# Change matchers
expect { cart.add_item(product) }.to change { cart.item_count }.by(1)
expect { account.withdraw(50) }.to change { account.balance }.from(100).to(50)
# Error matchers
expect { processor.execute }.to raise_error(ValidationError)
expect { parser.parse(invalid_data) }.to raise_error(ParseError, /invalid format/)
Cucumber brings natural language scenarios to Ruby applications. Business stakeholders write features in Gherkin syntax, and developers implement step definitions that connect the natural language to Ruby code:
# features/authentication.feature
Feature: User Login
Scenario: Login with valid credentials
Given a user exists with email "alice@example.com" and password "secret"
When I visit the login page
And I fill in "Email" with "alice@example.com"
And I fill in "Password" with "secret"
And I click "Sign In"
Then I should be on my dashboard
And I should see "Welcome back, Alice"
# features/step_definitions/authentication_steps.rb
Given('a user exists with email {string} and password {string}') do |email, password|
User.create!(email: email, password: password, name: 'Alice')
end
When('I visit the login page') do
visit login_path
end
When('I fill in {string} with {string}') do |field, value|
fill_in field, with: value
end
When('I click {string}') do |button|
click_button button
end
Then('I should be on my dashboard') do
expect(current_path).to eq(dashboard_path)
end
Then('I should see {string}') do |text|
expect(page).to have_content(text)
end
Step definitions use regular expressions or Cucumber expressions to extract parameters from natural language steps. The same step definitions can be reused across multiple scenarios, building a vocabulary of actions the system supports.
Capybara integrates with both RSpec and Cucumber to provide browser automation for acceptance testing:
RSpec.feature 'Product Search' do
scenario 'Finding products by category' do
create(:product, name: 'Ruby Programming', category: 'Books')
create(:product, name: 'Laptop Stand', category: 'Electronics')
visit products_path
select 'Books', from: 'Category'
click_button 'Search'
expect(page).to have_content('Ruby Programming')
expect(page).not_to have_content('Laptop Stand')
end
scenario 'Viewing product details', js: true do
product = create(:product, name: 'Wireless Mouse', description: 'Ergonomic design')
visit products_path
click_link 'Wireless Mouse'
expect(page).to have_css('h1', text: 'Wireless Mouse')
expect(page).to have_content('Ergonomic design')
end
end
Capybara abstracts browser interaction details, allowing tests to focus on user behavior rather than HTML structure. The framework supports multiple drivers including Selenium, Cuprite, and Rack::Test.
FactoryBot provides test data generation aligned with BDD principles:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password123' }
trait :admin do
role { :admin }
end
trait :with_orders do
after(:create) do |user|
create_list(:order, 3, user: user)
end
end
end
end
# Usage in specs
RSpec.describe 'Admin Dashboard' do
it 'displays user statistics' do
create_list(:user, 5)
create_list(:user, 2, :admin)
admin = create(:user, :admin)
sign_in(admin)
visit admin_dashboard_path
expect(page).to have_content('Total Users: 8')
end
end
Implementation Approaches
Organizations adopt BDD through different strategies depending on team structure, project constraints, and existing practices.
Scenario-First Approach begins by writing Gherkin scenarios that capture business requirements in natural language before any code exists. Teams hold specification workshops with business stakeholders to develop these scenarios collaboratively.
This approach works well when business stakeholders actively participate in defining requirements and teams need strong alignment between business goals and technical implementation. The natural language format enables non-technical stakeholders to review and validate specifications directly.
Implementation flows from scenarios to step definitions to application code:
Feature: Order Processing
Scenario: Applying bulk discount
Given I have a customer account
And my shopping cart contains:
| Item | Quantity | Price |
| Widget A | 50 | 10.00 |
| Widget B | 30 | 15.00 |
When I proceed to checkout
Then I should see a bulk discount of 10%
And my order total should be $675.00
Developers implement step definitions that translate these steps into executable code, driving the creation of domain objects and business logic necessary to fulfill the scenarios.
Spec-First Approach uses RSpec or similar frameworks to write executable specifications at the unit and integration level without natural language scenarios. Teams write specifications that describe object behavior, then implement code to satisfy those specifications.
This approach suits teams where developers maintain close communication with stakeholders through other means, or when the application domain involves complex technical concepts difficult to express in natural language:
RSpec.describe PricingEngine do
describe '#calculate_discount' do
context 'for bulk orders' do
it 'applies tiered discounts based on quantity' do
engine = PricingEngine.new
order = build_order(quantity: 50, unit_price: 10.00)
discount = engine.calculate_discount(order)
expect(discount.percentage).to eq(10)
expect(discount.amount).to eq(50.00)
end
end
context 'for small orders' do
it 'applies no discount below minimum quantity' do
engine = PricingEngine.new
order = build_order(quantity: 5, unit_price: 10.00)
discount = engine.calculate_discount(order)
expect(discount.percentage).to eq(0)
expect(discount.amount).to eq(0.00)
end
end
end
end
Hybrid Approach combines natural language scenarios for high-level acceptance criteria with spec-based tests for detailed component behavior. Teams write Cucumber scenarios describing user workflows and RSpec specifications testing individual objects.
Acceptance scenarios cover the happy path and critical edge cases from the user's perspective:
Scenario: Completing a purchase
Given I am logged in as a customer
And I have items in my shopping cart
When I provide valid payment information
And I confirm my order
Then I should receive an order confirmation
And I should receive a confirmation email
Unit specifications verify the components supporting these workflows:
RSpec.describe PaymentProcessor do
describe '#process' do
it 'charges the payment method' do
processor = PaymentProcessor.new
payment = build_payment(amount: 100.00, method: :credit_card)
result = processor.process(payment)
expect(result).to be_successful
expect(result.transaction_id).to be_present
end
end
end
This division keeps high-level tests focused on behavior while detailed tests verify implementation thoroughly.
Incremental Adoption introduces BDD gradually rather than converting entire projects simultaneously. Teams start by writing BDD-style tests for new features while maintaining existing test suites. Over time, as developers become comfortable with BDD practices, coverage expands.
Initial adoption often focuses on critical user workflows or areas with frequent defects. Teams gain experience with BDD tools and practices on manageable scope before expanding:
# New feature using BDD
RSpec.feature 'User Registration' do
scenario 'Successful registration with valid information' do
visit new_user_registration_path
fill_in 'Email', with: 'newuser@example.com'
fill_in 'Password', with: 'secure_password'
click_button 'Create Account'
expect(page).to have_content('Welcome!')
expect(User.find_by(email: 'newuser@example.com')).to be_present
end
end
# Existing feature with traditional tests (maintained as-is initially)
class UserTest < Minitest::Test
def test_user_creation
user = User.new(email: 'test@example.com', password: 'password')
assert user.save
assert_equal 'test@example.com', user.email
end
end
Tools & Ecosystem
The Ruby BDD ecosystem includes frameworks for different testing levels, tools for test data management, and utilities for improving test maintainability.
RSpec forms the foundation of Ruby BDD testing. The framework includes multiple components for different testing needs:
- rspec-core provides the test runner and specification structure
- rspec-expectations supplies the matcher library for assertions
- rspec-mocks offers test double capabilities
- rspec-rails integrates RSpec with Ruby on Rails applications
Configuration options control test execution behavior:
# spec/spec_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
expectations.syntax = :expect
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = 'spec/examples.txt'
config.disable_monkey_patching!
config.order = :random
Kernel.srand config.seed
end
Cucumber excels at acceptance testing with natural language scenarios. The framework parses Gherkin feature files and executes corresponding step definitions:
# Support for data tables in scenarios
Given('the following products exist:') do |table|
table.hashes.each do |row|
Product.create!(
name: row['Name'],
price: row['Price'].to_f,
category: row['Category']
)
end
end
# Background steps run before each scenario in a feature
Background:
Given the following products exist:
| Name | Price | Category |
| Ruby Book | 29.99 | Books |
| Python Book | 34.99 | Books |
| Laptop | 999.99| Electronics |
Capybara handles browser automation and page interaction. The framework supports multiple drivers:
# spec/rails_helper.rb
require 'capybara/rails'
require 'capybara/rspec'
Capybara.configure do |config|
config.default_driver = :rack_test
config.javascript_driver = :selenium_chrome_headless
config.default_max_wait_time = 5
end
# Using different drivers for different scenarios
RSpec.feature 'Interactive Features', js: true do
scenario 'Ajax-based search' do
visit search_path
fill_in 'Query', with: 'ruby'
# Wait for async results to load
expect(page).to have_css('.search-results', wait: 10)
end
end
FactoryBot creates test data with realistic attributes and relationships:
FactoryBot.define do
factory :order do
association :customer
status { :pending }
trait :completed do
status { :completed }
completed_at { Time.current }
end
factory :order_with_items do
transient do
items_count { 3 }
end
after(:create) do |order, evaluator|
create_list(:line_item, evaluator.items_count, order: order)
end
end
end
end
# Creating complex test data
order = create(:order_with_items, items_count: 5, customer: premium_customer)
DatabaseCleaner manages test database state between examples:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
VCR records HTTP interactions for deterministic external API testing:
VCR.configure do |config|
config.cassette_library_dir = 'spec/vcr_cassettes'
config.hook_into :webmock
config.configure_rspec_metadata!
end
RSpec.describe WeatherService do
it 'fetches current temperature', :vcr do
service = WeatherService.new
temperature = service.current_temperature(zip: '90210')
expect(temperature).to be_between(50, 100)
end
end
SimpleCov measures test coverage to identify untested code paths:
# spec/spec_helper.rb
require 'simplecov'
SimpleCov.start 'rails' do
add_filter '/spec/'
add_filter '/config/'
add_group 'Services', 'app/services'
add_group 'Presenters', 'app/presenters'
end
Practical Examples
Real-world BDD implementations demonstrate how the methodology addresses complex scenarios across different application types.
E-commerce Order Processing shows BDD handling multi-step workflows with business rules:
Feature: Order Fulfillment
As a warehouse manager
I want orders to be processed automatically
So that customers receive products quickly
Background:
Given the following products are in stock:
| SKU | Name | Quantity |
| BOOK01 | Ruby Guide | 100 |
| ELEC01 | USB Cable | 50 |
Scenario: Standard order fulfillment
Given a customer order contains:
| SKU | Quantity |
| BOOK01 | 2 |
| ELEC01 | 1 |
When the order is submitted
Then the order status should be "processing"
And inventory should be reserved:
| SKU | Reserved |
| BOOK01 | 2 |
| ELEC01 | 1 |
And a pick list should be generated
And the customer should receive a confirmation email
Scenario: Insufficient inventory
Given a customer order contains:
| SKU | Quantity |
| BOOK01 | 150 |
When the order is submitted
Then the order status should be "pending_inventory"
And the customer should be notified about the delay
And a restock notification should be created
Step definitions implement these scenarios:
Given('the following products are in stock:') do |table|
table.hashes.each do |row|
Product.create!(
sku: row['SKU'],
name: row['Name'],
stock_quantity: row['Quantity'].to_i
)
end
end
Given('a customer order contains:') do |table|
@order = Order.create!(customer: @current_customer || create(:customer))
table.hashes.each do |row|
product = Product.find_by(sku: row['SKU'])
@order.line_items.create!(product: product, quantity: row['Quantity'].to_i)
end
end
When('the order is submitted') do
@order.submit!
end
Then('inventory should be reserved:') do |table|
table.hashes.each do |row|
product = Product.find_by(sku: row['SKU'])
reservation = InventoryReservation.find_by(product: product, order: @order)
expect(reservation.quantity).to eq(row['Reserved'].to_i)
end
end
API Authentication System demonstrates BDD for security-critical features:
RSpec.describe 'API Authentication' do
describe 'POST /api/v1/tokens' do
context 'with valid credentials' do
it 'returns an access token and refresh token' do
user = create(:user, email: 'api@example.com', password: 'secure_pass')
post '/api/v1/tokens', params: {
email: 'api@example.com',
password: 'secure_pass'
}
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['access_token']).to be_present
expect(json['refresh_token']).to be_present
expect(json['expires_in']).to eq(3600)
end
it 'records the login timestamp' do
user = create(:user, email: 'api@example.com', password: 'secure_pass')
expect {
post '/api/v1/tokens', params: {
email: 'api@example.com',
password: 'secure_pass'
}
}.to change { user.reload.last_login_at }
end
end
context 'with invalid credentials' do
it 'returns unauthorized status' do
create(:user, email: 'api@example.com', password: 'secure_pass')
post '/api/v1/tokens', params: {
email: 'api@example.com',
password: 'wrong_password'
}
expect(response).to have_http_status(:unauthorized)
expect(JSON.parse(response.body)['error']).to eq('Invalid credentials')
end
it 'increments failed login counter' do
user = create(:user, email: 'api@example.com', password: 'secure_pass')
expect {
post '/api/v1/tokens', params: {
email: 'api@example.com',
password: 'wrong_password'
}
}.to change { user.reload.failed_login_attempts }.by(1)
end
end
context 'with locked account' do
it 'rejects authentication attempts' do
user = create(:user, :locked, email: 'api@example.com')
post '/api/v1/tokens', params: {
email: 'api@example.com',
password: 'secure_pass'
}
expect(response).to have_http_status(:forbidden)
expect(JSON.parse(response.body)['error']).to eq('Account locked')
end
end
end
end
Background Job Processing illustrates BDD for asynchronous operations:
RSpec.describe 'Report Generation' do
describe 'generating sales reports' do
context 'with valid date range' do
it 'creates a report job' do
expect {
post '/reports', params: {
type: 'sales',
start_date: '2025-01-01',
end_date: '2025-01-31'
}
}.to have_enqueued_job(GenerateReportJob)
end
it 'notifies user when report is ready', :perform_enqueued_jobs do
user = create(:user, email: 'manager@example.com')
sign_in(user)
post '/reports', params: {
type: 'sales',
start_date: '2025-01-01',
end_date: '2025-01-31'
}
expect(ActionMailer::Base.deliveries.last.to).to include('manager@example.com')
expect(ActionMailer::Base.deliveries.last.subject).to include('Report Ready')
end
end
context 'when report generation fails' do
it 'retries the job', :perform_enqueued_jobs do
allow_any_instance_of(ReportGenerator).to receive(:generate).and_raise(StandardError)
post '/reports', params: {
type: 'sales',
start_date: '2025-01-01',
end_date: '2025-01-31'
}
expect(GenerateReportJob).to have_been_enqueued.at_least(3).times
end
end
end
end
Common Pitfalls
BDD implementations encounter recurring problems that undermine the methodology's benefits. Recognizing these pitfalls helps teams maintain effective BDD practices.
Overly Technical Scenarios defeat the purpose of natural language specifications when they contain implementation details. Scenarios should describe user behavior, not technical mechanisms:
# Poor: Implementation details leak into scenario
Scenario: User registration
Given I POST to "/api/users" with JSON payload
And the database table "users" is empty
When the UserCreationService processes the request
Then a record should exist in "users" table
And the password_digest column should be populated
# Better: Focus on user-visible behavior
Scenario: User registration
Given I am on the registration page
When I sign up with email "user@example.com"
And I provide a valid password
Then I should be logged in
And I should see a welcome message
Technical terminology makes scenarios unreadable to business stakeholders and ties specifications to implementation details that may change.
Testing Implementation Rather Than Behavior occurs when specifications verify internal object state instead of observable outcomes:
# Poor: Testing implementation details
RSpec.describe Order do
it 'sets the internal state machine to submitted' do
order = Order.new
order.submit
expect(order.instance_variable_get(:@state)).to eq(:submitted)
end
end
# Better: Testing behavior
RSpec.describe Order do
it 'prevents modifications after submission' do
order = Order.new
order.submit
expect { order.add_item(product) }.to raise_error(Order::AlreadySubmitted)
end
end
Behavior-focused tests remain valid when refactoring implementation, while implementation-focused tests break during routine code changes.
Duplicate Step Definitions create maintenance burden when multiple step definitions do the same thing with slightly different wording:
# Poor: Multiple steps for the same action
Given('I am logged in') do
login_as(create(:user))
end
Given('I have signed in') do
login_as(create(:user))
end
Given('I am authenticated') do
login_as(create(:user))
end
# Better: Single step, consistent wording
Given('I am logged in as {string}') do |role|
user = create(:user, role: role)
login_as(user)
end
Teams should establish conventions for common actions and reuse step definitions across scenarios.
Insufficient Isolation Between Tests causes failures when tests depend on execution order or shared state:
# Poor: Tests share state
RSpec.describe ShoppingCart do
before(:all) do
@cart = ShoppingCart.new
end
it 'adds items' do
@cart.add_item(create(:product))
expect(@cart.item_count).to eq(1)
end
it 'calculates total' do
# Fails if previous test didn't run first
expect(@cart.total).to be > 0
end
end
# Better: Each test is independent
RSpec.describe ShoppingCart do
it 'adds items' do
cart = ShoppingCart.new
cart.add_item(create(:product, price: 10.00))
expect(cart.item_count).to eq(1)
end
it 'calculates total' do
cart = ShoppingCart.new
cart.add_item(create(:product, price: 10.00))
expect(cart.total).to eq(10.00)
end
end
Imperative Rather Than Declarative Scenarios describe every UI interaction instead of focusing on intent:
# Poor: Imperative steps describing mechanics
Scenario: Adding a product to cart
Given I am on the homepage
When I click the "Products" link
And I scroll down to "Ruby Book"
And I click "View Details"
And I click "Add to Cart"
And I click the cart icon
Then I should see "Ruby Book" in the cart
# Better: Declarative steps describing intent
Scenario: Adding a product to cart
Given I am browsing products
When I add "Ruby Book" to my cart
Then my cart should contain "Ruby Book"
Declarative scenarios remain valid when UI implementation changes, while imperative scenarios require updates for every interface modification.
Vague or Ambiguous Step Definitions create confusion and make scenarios difficult to implement:
# Poor: Vague implementation
When('I submit the form') do
click_button 'Submit'
end
# Better: Clear about which form
When('I submit the registration form') do
within('#registration-form') do
click_button 'Create Account'
end
end
When('I submit the search form') do
within('#search-form') do
click_button 'Search'
end
end
Testing Through the UI Exclusively makes tests slow and brittle. Not all scenarios require full browser automation:
# Slow: Every test goes through UI
RSpec.feature 'Order Processing' do
scenario 'calculating discount' do
visit new_order_path
fill_in 'Customer', with: 'Premium Customer'
fill_in 'Amount', with: '1000'
click_button 'Calculate'
expect(page).to have_content('Discount: $100')
end
end
# Fast: Test business logic directly
RSpec.describe DiscountCalculator do
it 'applies premium customer discount' do
calculator = DiscountCalculator.new
order = build(:order, customer_type: :premium, amount: 1000)
discount = calculator.calculate(order)
expect(discount).to eq(100)
end
end
# UI test covers integration only
RSpec.feature 'Order Processing' do
scenario 'displaying calculated discount' do
visit new_order_path
fill_in 'Amount', with: '1000'
select 'Premium', from: 'Customer Type'
click_button 'Calculate'
expect(page).to have_content('Discount: $100')
end
end
Reference
BDD Frameworks Comparison
| Framework | Purpose | Syntax Style | Typical Use Case |
|---|---|---|---|
| RSpec | Unit and integration testing | Specification DSL | Component behavior verification |
| Cucumber | Acceptance testing | Natural language (Gherkin) | Business-readable scenarios |
| Capybara | Browser automation | Ruby DSL | User interaction simulation |
| Minitest::Spec | Unit testing | Specification DSL | Lightweight behavioral testing |
Gherkin Keywords
| Keyword | Purpose | Example |
|---|---|---|
| Feature | Describes functionality being tested | Feature: User Authentication |
| Background | Steps run before each scenario | Background: Given a registered user |
| Scenario | Concrete example of behavior | Scenario: Login with valid credentials |
| Given | Establishes context | Given I am on the login page |
| When | Describes action | When I enter my credentials |
| Then | States expected outcome | Then I should see my dashboard |
| And | Continues previous step type | And I should see a welcome message |
| But | Negative continuation | But I should not see login form |
RSpec Structure Components
| Component | Purpose | Example |
|---|---|---|
| describe | Groups related specifications | describe Order do |
| context | Describes specific condition | context 'when empty' do |
| it | States expected behavior | it 'returns zero' do |
| before | Setup run before examples | before { create(:user) } |
| after | Cleanup run after examples | after { clear_cache } |
| let | Lazy-evaluated memoized helper | let(:user) { create(:user) } |
| let! | Eager-evaluated helper | let!(:product) { create(:product) } |
| subject | Declares example subject | subject { Order.new } |
Common RSpec Matchers
| Matcher | Purpose | Example |
|---|---|---|
| eq | Exact equality | expect(result).to eq(42) |
| be | Identity comparison | expect(obj).to be(same_obj) |
| be_nil | Nil check | expect(value).to be_nil |
| be_truthy | Truthiness check | expect(condition).to be_truthy |
| include | Collection membership | expect(array).to include(item) |
| match | Regex matching | expect(text).to match(/pattern/) |
| raise_error | Exception expectation | expect { code }.to raise_error(Error) |
| change | State change verification | expect { action }.to change { count }.by(1) |
| have_attributes | Attribute verification | expect(obj).to have_attributes(name: 'Test') |
| be_within | Approximate equality | expect(value).to be_within(0.1).of(3.14) |
Capybara Actions
| Action | Purpose | Example |
|---|---|---|
| visit | Navigate to path | visit products_path |
| click_link | Click link by text | click_link 'View Details' |
| click_button | Click button by text | click_button 'Submit' |
| fill_in | Enter text in field | fill_in 'Email', with: 'user@example.com' |
| select | Choose from dropdown | select 'Premium', from: 'Membership' |
| check | Check checkbox | check 'Terms of Service' |
| choose | Select radio button | choose 'Credit Card' |
| attach_file | Upload file | attach_file 'Avatar', file_path |
Capybara Matchers
| Matcher | Purpose | Example |
|---|---|---|
| have_content | Text presence | expect(page).to have_content('Welcome') |
| have_css | CSS selector | expect(page).to have_css('.error') |
| have_selector | Generic selector | expect(page).to have_selector('h1', text: 'Title') |
| have_link | Link presence | expect(page).to have_link('Sign Out') |
| have_button | Button presence | expect(page).to have_button('Submit') |
| have_field | Form field presence | expect(page).to have_field('Email') |
| have_current_path | URL verification | expect(page).to have_current_path(dashboard_path) |
FactoryBot Patterns
| Pattern | Purpose | Example |
|---|---|---|
| create | Build and save record | create(:user) |
| build | Build without saving | build(:user) |
| build_stubbed | Build with stubbed attributes | build_stubbed(:user) |
| create_list | Create multiple records | create_list(:product, 5) |
| trait | Define attribute variation | factory :user do trait :admin |
| transient | Define build-time parameters | transient do items_count { 3 } end |
| association | Define relationship | association :author, factory: :user |
| sequence | Generate unique values | sequence(:email) { "user#{n}@example.com" } |
BDD Best Practices Checklist
| Practice | Description |
|---|---|
| Write scenarios before code | Define expected behavior before implementation |
| Use ubiquitous language | Match domain terminology in all artifacts |
| Keep scenarios declarative | Describe what, not how |
| One behavior per scenario | Each scenario tests single outcome |
| Maintain test independence | Each test runs successfully in isolation |
| Test behavior, not implementation | Verify observable outcomes |
| Use appropriate test level | Unit tests for logic, acceptance for workflows |
| Keep step definitions reusable | Share common steps across scenarios |
| Run tests frequently | Catch regressions early |
| Refactor tests with code | Keep specifications maintainable |