Testing Methodologies
Testing methodologies provide structured approaches to writing tests and developing software. This guide covers Test-Driven Development (TDD) and Behavior-Driven Development (BDD).
Test-Driven Development (TDD)
Test-Driven Development is a software development approach where tests are written before the actual code.
The Red-Green-Refactor Cycle
TDD follows a simple three-step cycle:
🔴 Red → 🟢 Green → 🔵 Refactor → 🔴 Red → ...1. 🔴 Red: Write a Failing Test
Write a test for the next bit of functionality you want to add. The test should fail because the functionality doesn’t exist yet.
// sum.test.ts
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(2, 3)).toBe(5) // This will fail - sum() doesn't exist yet
})
})2. 🟢 Green: Write Minimal Code to Pass
Write just enough code to make the test pass. Don’t worry about perfect code yet.
// sum.ts
export function sum(a: number, b: number): number {
return a + b // Simplest implementation to pass the test
}3. 🔵 Refactor: Improve the Code
Now that the test passes, improve the code quality while keeping the test green.
// sum.ts (refactored)
export function sum(...numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0)
}
// Update test to cover new functionality
it('should add multiple numbers', () => {
expect(sum(1, 2, 3, 4)).toBe(10)
})TDD in Practice: Building a Shopping Cart
Let’s build a shopping cart feature using TDD:
Step 1: First Test (Red)
// cart.test.ts
import { describe, it, expect } from 'vitest'
import { Cart } from './cart'
describe('Cart', () => {
it('should start with zero items', () => {
const cart = new Cart()
expect(cart.itemCount()).toBe(0)
})
})Step 2: Minimal Implementation (Green)
// cart.ts
export class Cart {
private items: any[] = []
itemCount(): number {
return this.items.length
}
}Step 3: Add More Tests (Red)
it('should add item to cart', () => {
const cart = new Cart()
cart.addItem({ id: 1, name: 'Book', price: 10 })
expect(cart.itemCount()).toBe(1)
})
it('should calculate total price', () => {
const cart = new Cart()
cart.addItem({ id: 1, name: 'Book', price: 10 })
cart.addItem({ id: 2, name: 'Pen', price: 5 })
expect(cart.getTotalPrice()).toBe(15)
})Step 4: Implement Features (Green)
// cart.ts
interface CartItem {
id: number
name: string
price: number
}
export class Cart {
private items: CartItem[] = []
addItem(item: CartItem): void {
this.items.push(item)
}
itemCount(): number {
return this.items.length
}
getTotalPrice(): number {
return this.items.reduce((total, item) => total + item.price, 0)
}
}Step 5: Refactor (Blue)
// cart.ts (refactored with better types and methods)
interface CartItem {
id: number
name: string
price: number
quantity?: number
}
export class Cart {
private items: Map<number, CartItem> = new Map()
addItem(item: CartItem): void {
const existingItem = this.items.get(item.id)
if (existingItem) {
existingItem.quantity = (existingItem.quantity || 1) + 1
} else {
this.items.set(item.id, { ...item, quantity: 1 })
}
}
removeItem(itemId: number): void {
this.items.delete(itemId)
}
itemCount(): number {
return Array.from(this.items.values()).reduce(
(count, item) => count + (item.quantity || 1),
0
)
}
getTotalPrice(): number {
return Array.from(this.items.values()).reduce(
(total, item) => total + item.price * (item.quantity || 1),
0
)
}
}Benefits of TDD
✅ Better Design: Writing tests first leads to more testable, modular code
✅ Living Documentation: Tests serve as up-to-date documentation
✅ Fewer Bugs: Issues are caught early in development
✅ Confidence: Refactor without fear of breaking things
✅ Focus: Clear goal for each coding session
Drawbacks of TDD
❌ Learning Curve: Takes time to master the workflow
❌ Initial Slowdown: Feels slower at first (but pays off long-term)
❌ Not Always Practical: UI or exploratory work may not fit TDD well
❌ Maintenance: Tests need updating as requirements change
TDD Best Practices
- Write the smallest test possible
- Make it fail first to ensure the test actually tests something
- Write minimal code to pass the test
- Refactor both production and test code
- Commit after each green phase
- Test one thing at a time
- Keep tests fast to run them frequently
TDD is a discipline that takes practice. Start with simple functions and gradually apply it to more complex features.
Behavior-Driven Development (BDD)
BDD extends TDD by focusing on the behavior of the application from the user’s perspective. It uses natural language to describe tests.
Key Concepts
- Collaboration: Developers, QA, and stakeholders work together
- Ubiquitous Language: Shared vocabulary everyone understands
- User Focus: Tests describe user behavior, not implementation
- Living Documentation: Feature files serve as requirements
Given-When-Then Syntax
BDD tests follow a structured format:
- Given: Initial context (the setup)
- When: Action or event (what happens)
- Then: Expected outcome (the verification)
Example Structure
Given [some context or initial state]
When [an event or action occurs]
Then [ensure some outcome]Gherkin Language
Gherkin is a domain-specific language for writing BDD tests in plain English.
Basic Gherkin Syntax
Feature: User Login
As a registered user
I want to log into my account
So that I can access my dashboard
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter valid credentials
| email | password |
| user@example.com | Pass123! |
And I click the "Login" button
Then I should be redirected to the dashboard
And I should see a welcome message
Scenario: Failed login with invalid password
Given I am on the login page
When I enter invalid credentials
| email | password |
| user@example.com | wrong |
And I click the "Login" button
Then I should see an error message "Invalid credentials"
And I should remain on the login pageGherkin Keywords
| Keyword | Purpose |
|---|---|
Feature | High-level description of functionality |
Scenario | Concrete example of behavior |
Given | Initial context/precondition |
When | Event or action |
Then | Expected outcome |
And | Additional step (continues previous type) |
But | Negative additional step |
Background | Steps that run before every scenario |
Examples | Data table for scenario outlines |
Cucumber Framework
Cucumber is a popular BDD framework that executes Gherkin feature files.
Installing Cucumber (JavaScript)
npm install --save-dev @cucumber/cucumberProject Structure
features/
├── login.feature
└── step_definitions/
└── login.steps.ts
support/
└── world.ts
cucumber.jsWriting Feature Files
# features/shopping-cart.feature
Feature: Shopping Cart
As a customer
I want to manage items in my cart
So that I can purchase products
Background:
Given I am logged in as a customer
Scenario: Add item to cart
Given I am viewing product "Wireless Mouse"
When I click "Add to Cart"
Then the cart should contain 1 item
And the cart total should be "$29.99"
Scenario: Remove item from cart
Given I have added "Wireless Mouse" to my cart
When I remove "Wireless Mouse" from my cart
Then the cart should be empty
And the cart total should be "$0.00"
Scenario Outline: Add multiple quantities
Given I am viewing product "<product>"
When I add <quantity> items to cart
Then the cart should contain <quantity> items
Examples:
| product | quantity |
| USB Cable | 2 |
| HDMI Adapter | 3 |
| Laptop Stand | 1 |Writing Step Definitions
// features/step_definitions/shopping-cart.steps.ts
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@jest/globals'
Given('I am logged in as a customer', async function () {
await this.loginAsCustomer('customer@example.com', 'password123')
})
Given('I am viewing product {string}', async function (productName: string) {
await this.navigateToProduct(productName)
})
When('I click {string}', async function (buttonText: string) {
await this.clickButton(buttonText)
})
When('I add {int} items to cart', async function (quantity: number) {
await this.setQuantity(quantity)
await this.clickButton('Add to Cart')
})
When('I remove {string} from my cart', async function (productName: string) {
await this.removeFromCart(productName)
})
Then('the cart should contain {int} item(s)', async function (count: number) {
const itemCount = await this.getCartItemCount()
expect(itemCount).toBe(count)
})
Then('the cart should be empty', async function () {
const isEmpty = await this.isCartEmpty()
expect(isEmpty).toBe(true)
})
Then('the cart total should be {string}', async function (total: string) {
const cartTotal = await this.getCartTotal()
expect(cartTotal).toBe(total)
})World Object (Shared Context)
// support/world.ts
import { setWorldConstructor, World } from '@cucumber/cucumber'
import { Browser, Page, chromium } from 'playwright'
export class CustomWorld extends World {
browser!: Browser
page!: Page
async init() {
this.browser = await chromium.launch()
this.page = await this.browser.newPage()
}
async cleanup() {
await this.page.close()
await this.browser.close()
}
async navigateToProduct(productName: string) {
await this.page.goto(`/products/${productName}`)
}
async clickButton(buttonText: string) {
await this.page.click(`button:has-text("${buttonText}")`)
}
async getCartItemCount(): Promise<number> {
const text = await this.page.textContent('.cart-count')
return parseInt(text || '0', 10)
}
}
setWorldConstructor(CustomWorld)BDD with Jest
You can practice BDD principles without Cucumber by structuring Jest tests with Given-When-Then.
Example: BDD-Style Jest Tests
// login.spec.ts
import { describe, it, expect, beforeEach } from '@jest/globals'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginPage from './LoginPage'
describe('Feature: User Login', () => {
describe('Scenario: Successful login with valid credentials', () => {
let user: ReturnType<typeof userEvent.setup>
beforeEach(() => {
user = userEvent.setup()
})
it('should redirect to dashboard when user enters valid credentials', async () => {
// Given I am on the login page
render(<LoginPage />)
// When I enter valid credentials
const emailInput = screen.getByLabelText(/email/i)
const passwordInput = screen.getByLabelText(/password/i)
await user.type(emailInput, 'user@example.com')
await user.type(passwordInput, 'Pass123!')
// And I click the Login button
const loginButton = screen.getByRole('button', { name: /login/i })
await user.click(loginButton)
// Then I should be redirected to the dashboard
expect(window.location.pathname).toBe('/dashboard')
// And I should see a welcome message
expect(await screen.findByText(/welcome back/i)).toBeInTheDocument()
})
})
describe('Scenario: Failed login with invalid password', () => {
it('should show error message when password is incorrect', async () => {
const user = userEvent.setup()
// Given I am on the login page
render(<LoginPage />)
// When I enter invalid credentials
await user.type(screen.getByLabelText(/email/i), 'user@example.com')
await user.type(screen.getByLabelText(/password/i), 'wrongpassword')
// And I click the Login button
await user.click(screen.getByRole('button', { name: /login/i }))
// Then I should see an error message
expect(
await screen.findByText(/invalid credentials/i)
).toBeInTheDocument()
// And I should remain on the login page
expect(window.location.pathname).toBe('/login')
})
})
})BDD Test Structure with Comments
describe('Feature: Shopping Cart Management', () => {
describe('Scenario: Adding items to cart', () => {
it('should increase cart count when product is added', async () => {
// GIVEN a customer is viewing a product
const product = { id: 1, name: 'Laptop', price: 999 }
render(<ProductPage product={product} />)
// WHEN the customer clicks "Add to Cart"
const addButton = screen.getByRole('button', { name: /add to cart/i })
await userEvent.click(addButton)
// THEN the cart count should increase
const cartBadge = screen.getByTestId('cart-count')
expect(cartBadge).toHaveTextContent('1')
// AND a success notification should appear
expect(screen.getByText(/added to cart/i)).toBeInTheDocument()
})
})
})Collaboration Workflow
BDD shines when teams collaborate on feature specifications.
Three Amigos Meeting
The “Three Amigos” are:
- Developer: Builds the feature
- Tester (QA): Validates the feature
- Product Owner/BA: Defines the feature
Collaboration Process
- Discovery: Discuss the feature and write examples
- Formulation: Convert examples into Gherkin scenarios
- Automation: Implement step definitions
- Validation: Run tests and verify behavior
Example Collaboration Session
Feature: Password Reset
As a user who forgot their password
I want to reset it via email
So that I can regain access to my account
# Developer asks: What if email doesn't exist?
Scenario: Request reset for non-existent email
Given I am on the password reset page
When I enter email "nonexistent@example.com"
And I click "Send Reset Link"
Then I should see "If that email exists, a reset link was sent"
# Security: Don't reveal whether email exists
# QA asks: What about rate limiting?
Scenario: Multiple reset requests in short time
Given I requested a password reset 2 minutes ago
When I request another password reset
Then I should see "Please wait before requesting another reset"
# Product Owner adds: Link should expire
Scenario: Using expired reset link
Given I received a password reset link 25 hours ago
When I click the reset link
Then I should see "This reset link has expired"
And I should be able to request a new linkBDD vs TDD Comparison
| Aspect | TDD | BDD |
|---|---|---|
| Focus | How code works | How system behaves |
| Language | Technical (code) | Natural (business) |
| Audience | Developers | Developers, QA, stakeholders |
| Tests | Unit/Integration | Acceptance/Integration |
| Tools | Jest, Vitest, Mocha | Cucumber, SpecFlow, Behave |
| Starting | Write failing test | Write feature scenario |
| Goal | Correct implementation | Correct behavior |
| Granularity | Fine (functions/methods) | Coarse (features/user stories) |
When to Use TDD
- Internal libraries and utilities
- Complex algorithms and business logic
- API development
- Low-level component behavior
When to Use BDD
- User-facing features
- Cross-functional requirements
- Acceptance criteria validation
- Stakeholder communication
TDD and BDD are not mutually exclusive. Many teams use both: BDD for high-level acceptance tests and TDD for implementation details.
Practical Example: Building a Feature with BDD
1. Write Feature File
# features/search.feature
Feature: Product Search
As a customer
I want to search for products
So that I can find what I'm looking for
Scenario: Search with results
Given there are products in the catalog
When I search for "laptop"
Then I should see at least 1 search result
And each result should contain "laptop" in name or description
Scenario: Search with no results
Given there are products in the catalog
When I search for "nonexistentproduct123"
Then I should see "No results found"
And I should see search suggestions2. Implement Step Definitions
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@jest/globals'
Given('there are products in the catalog', async function () {
await this.seedProducts([
{ name: 'Gaming Laptop', description: 'High-performance laptop' },
{ name: 'Office Mouse', description: 'Ergonomic mouse' },
])
})
When('I search for {string}', async function (query: string) {
await this.page.fill('[data-testid="search-input"]', query)
await this.page.click('[data-testid="search-button"]')
})
Then(
'I should see at least {int} search result(s)',
async function (count: number) {
const results = await this.page.locator('[data-testid="result-item"]')
expect(await results.count()).toBeGreaterThanOrEqual(count)
}
)3. Implement Feature
// SearchPage.tsx
export default function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const handleSearch = async () => {
const data = await fetch(`/api/search?q=${query}`)
const products = await data.json()
setResults(products)
}
return (
<div>
<input
data-testid="search-input"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button data-testid="search-button" onClick={handleSearch}>
Search
</button>
{results.length === 0 ? (
<p>No results found</p>
) : (
results.map((product) => (
<div key={product.id} data-testid="result-item">
{product.name}
</div>
))
)}
</div>
)
}Best Practices
For TDD
- Write the test you wish you had
- See it fail for the right reason
- Make it pass quickly
- Refactor carefully
- Repeat
For BDD
- Focus on user value
- Use concrete examples
- Keep scenarios independent
- Avoid technical details in feature files
- Maintain living documentation
- Involve the whole team
Don’t write BDD tests for everything. Reserve them for important user-facing features and use unit tests for implementation details.
Next Steps
- Explore Testing Terminology to understand common terms
- Learn JavaScript Testing with Jest and Vitest
- Dive into End-to-End Testing with Cucumber integration