TestโDriven Development (TDD) and BehaviorโDriven Development (BDD) are complementary testing approaches that help teams build robust, maintainable software by defining expected behaviour before writing production code. In TDD, developers write small, focused unit tests that fail initially, then implement just enough code to make them pass, ensuring each component meets its specification. BDD extends this idea by framing tests in a global language that all stakeholdersโdevelopers, QA, and product ownersโcan understand, using human-readable scenarios to describe system behaviour. While TDD emphasizes the correctness of individual units, BDD elevates collaboration and shared understanding by specifying the “why” and “how” of features in a narrative style, driving development through concrete examples of desired outcomes.
๐ TDD vs ๐ญ BDD: Methodologies vs Frameworks
๐ง Understanding the Concepts
๐ TDD (Test Driven Development)
- Methodology/Process: Write test โ Fail โ Write code โ Pass โ Refactor
- Focus: Testing the implementation and correctness
- Mindset: “Does this code work correctly?”
- Style: More technical, code-focused
๐ญ BDD (Behavior Driven Development)
- Methodology/Process: Describe behavior โ Write specs โ Implement โ Verify behavior
- Focus: Testing the behavior and user requirements
- Mindset: “Does this behave as expected from user’s perspective?”
- Style: More natural language, business-focused
๐ ๏ธ Frameworks Support Both Approaches
๐ RSpec (Primarily BDD-oriented)
# BDD Style - describing behavior
describe "TwoSum" do
context "when given an empty array" do
it "should inform user about insufficient data" do
expect(two_sum([], 9)).to eq('Provide an array with length 2 or more')
end
end
end
โ๏ธ Minitest (Supports Both TDD and BDD)
๐ง TDD Style with Minitest
class TestTwoSum < Minitest::Test
# Testing implementation correctness
def test_empty_array_returns_error
assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
end
def test_valid_input_returns_indices
assert_equal [0, 1], two_sum([2, 7], 9)
end
end
๐ญ BDD Style with Minitest
describe "TwoSum behavior" do
describe "when user provides empty array" do
it "guides user to provide sufficient data" do
_(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
end
end
describe "when user provides valid input" do
it "finds the correct pair indices" do
_(two_sum([2, 7], 9)).must_equal [0, 1]
end
end
end
๐ฏ Key Differences in Practice
๐ TDD Approach
# 1. Write failing test
def test_two_sum_with_valid_input
assert_equal [0, 1], two_sum([2, 7], 9) # This will fail initially
end
# 2. Write minimal code to pass
def two_sum(nums, target)
[0, 1] # Hardcoded to pass
end
# 3. Refactor and improve
def two_sum(nums, target)
# Actual implementation
end
๐ญ BDD Approach
# 1. Describe the behavior first
describe "Finding two numbers that sum to target" do
context "when valid numbers exist" do
it "returns their indices" do
# This describes WHAT should happen, not HOW
expect(two_sum([2, 7, 11, 15], 9)).to eq([0, 1])
end
end
end
๐ Summary Table
| Aspect | TDD | BDD |
|---|---|---|
| Focus | Implementation correctness | User behavior |
| Language | Technical | Business/Natural |
| Frameworks | Any (Minitest, RSpec, etc.) | Any (RSpec, Minitest spec, etc.) |
| Test Names | test_method_returns_value | "it should behave like..." |
| Audience | Developers | Stakeholders + Developers |
๐ช The Reality
- RSpec encourages BDD but can be used for TDD
- Minitest is framework-agnostic – supports both approaches equally
- Your choice of methodology (TDD vs BDD) is independent of your framework choice
- Many teams use hybrid approaches – BDD for acceptance tests, TDD for unit tests
The syntax doesn’t determine the methodology – it’s about how you think and approach the problem!
System Tests ๐ปโ๏ธ
System tests in Rails (located in test/system/*) are full-stack integration tests that simulate real user interactions with your web application. They’re the highest level of testing in the Rails testing hierarchy and provide the most realistic testing environment.
System tests actually launch a real web browser (or headless browser) and interact with your application just like a real user would. Looking at our Rails app’s configuration: design_studio/test/application_system_test_case.rb
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
This means our system tests run using:
- Selenium WebDriver (browser automation tool)
- Headless Chrome (Chrome browser without UI)
- 1400×1400 screen size for consistent testing
Code Snippets from:actionpack-8.0.2/lib/action_dispatch/system_test_case.rb
# frozen_string_literal: true
# :markup: markdown
gem "capybara", ">= 3.26"
require "capybara/dsl"
require "capybara/minitest"
require "action_controller"
require "action_dispatch/system_testing/driver"
require "action_dispatch/system_testing/browser"
require "action_dispatch/system_testing/server"
require "action_dispatch/system_testing/test_helpers/screenshot_helper"
require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
module ActionDispatch
# # System Testing
#
# System tests let you test applications in the browser. Because system tests
# use a real browser experience, you can test all of your JavaScript easily from
# your test suite.
#
# To create a system test in your application, extend your test class from
# `ApplicationSystemTestCase`. System tests use Capybara as a base and allow you
# to configure the settings through your `application_system_test_case.rb` file
# that is generated with a new application or scaffold.
#
# Here is an example system test:
#
# require "application_system_test_case"
#
# class Users::CreateTest < ApplicationSystemTestCase
# test "adding a new user" do
# visit users_path
# click_on 'New User'
#
# fill_in 'Name', with: 'Arya'
# click_on 'Create User'
#
# assert_text 'Arya'
# end
# end
#
# When generating an application or scaffold, an
# `application_system_test_case.rb` file will also be generated containing the
# base class for system testing. This is where you can change the driver, add
# Capybara settings, and other configuration for your system tests.
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
# end
#
# By default, `ActionDispatch::SystemTestCase` is driven by the Selenium driver,
# with the Chrome browser, and a browser size of 1400x1400.
#
# Changing the driver configuration options is easy. Let's say you want to use
# the Firefox browser instead of Chrome. In your
# `application_system_test_case.rb` file add the following:
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :firefox
# end
#
# `driven_by` has a required argument for the driver name. The keyword arguments
# are `:using` for the browser and `:screen_size` to change the size of the
# browser screen. These two options are not applicable for headless drivers and
# will be silently ignored if passed.
#
# Headless browsers such as headless Chrome and headless Firefox are also
# supported. You can use these browsers by setting the `:using` argument to
# `:headless_chrome` or `:headless_firefox`.
#
# To use a headless driver, like Cuprite, update your Gemfile to use Cuprite
# instead of Selenium and then declare the driver name in the
# `application_system_test_case.rb` file. In this case, you would leave out the
# `:using` option because the driver is headless, but you can still use
# `:screen_size` to change the size of the browser screen, also you can use
# `:options` to pass options supported by the driver. Please refer to your
# driver documentation to learn about supported options.
#
# require "test_helper"
# require "capybara/cuprite"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :cuprite, screen_size: [1400, 1400], options:
# { js_errors: true }
# end
#
# Some drivers require browser capabilities to be passed as a block instead of
# through the `options` hash.
#
# As an example, if you want to add mobile emulation on chrome, you'll have to
# create an instance of selenium's `Chrome::Options` object and add capabilities
# with a block.
#
# The block will be passed an instance of `<Driver>::Options` where you can
# define the capabilities you want. Please refer to your driver documentation to
# learn about supported options.
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
# driver_option.add_emulation(device_name: 'iPhone 6')
# driver_option.add_extension('path/to/chrome_extension.crx')
# end
# end
#
# Because `ActionDispatch::SystemTestCase` is a shim between Capybara and Rails,
# any driver that is supported by Capybara is supported by system tests as long
# as you include the required gems and files.
class SystemTestCase < ActiveSupport::TestCase
include Capybara::DSL
include Capybara::Minitest::Assertions
include SystemTesting::TestHelpers::SetupAndTeardown
include SystemTesting::TestHelpers::ScreenshotHelper
..........
How They Work
System tests can:
- Navigate pages:
visit products_url - Click elements:
click_on "New product" - Fill forms:
fill_in "Title", with: @product.title - Verify content:
assert_text "Product was successfully created" - Check page structure:
assert_selector "h1", text: "Products"
Examples From Our Codebase
Basic navigation test (from products_test.rb):
test "visiting the index" do
visit products_url
assert_selector "h1", text: "Products"
end
Complex user workflow (from profile_test.rb):
def sign_in_user(user)
visit new_session_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_button "Log In"
# Wait for redirect and verify we're not on the login page anymore
# Also wait for the success notice to appear
assert_text "Logged in successfully", wait: 10
assert_no_text "Log in to your account", wait: 5
end
Key Benefits
- End-to-end testing: Tests the complete user journey
- JavaScript testing: Can test dynamic frontend behavior
- Real browser environment: Tests CSS, responsive design, and browser compatibility
- User perspective: Validates the actual user experience
When to Use System Tests
- Critical user workflows (login, checkout, registration)
- Complex page interactions (forms, modals, AJAX)
- Cross-browser compatibility
- Responsive design validation
Our profile_test.rb is a great example – it tests the entire user authentication flow, profile page navigation, and various UI interactions that a real user would perform.
Happy Testing ๐