Skip to main content

Command Palette

Search for a command to run...

TDD vs BDD with Amazon Q Developer: Why Test-Driven Development Shines with Code Assistants

Updated
5 min read
TDD vs BDD with Amazon Q Developer: Why Test-Driven Development Shines with Code Assistants
D
I'm Ayyanar Jeyakrishnan ; aka AJ. With over 21 years in IT, I'm a passionate Multi-Cloud Architect specialising in crafting scalable and efficient cloud solutions. I've successfully designed and implemented multi-cloud architectures for diverse organisations, harnessing AWS, Azure, and GCP. My track record includes delivering Machine Learning and Data Platform projects with a focus on high availability, security, and scalability. I'm a proponent of DevOps and MLOps methodologies, accelerating development and deployment. I actively engage with the tech community, sharing knowledge in sessions, conferences, and mentoring programs. Constantly learning and pursuing certifications, I provide cutting-edge solutions to drive success in the evolving cloud and AI/ML landscape.

The Context Advantage: A Developer's Story

As I sat down to build the Simple Budget Tracker application with Amazon Q Developer, I faced a critical decision at the outset: should I follow Behavior-Driven Development (BDD) or Test-Driven Development (TDD)? While both methodologies have their merits, my experience revealed that TDD offers distinct advantages when working with Large Language Model (LLM) based coding assistants like Amazon Q.

You can refer the Blog 1: Building a Full-Stack Budget Tracker Application with Amazon Q CLI: An Iterative Approach

You can refer the Blog 2: Deploying a Flask Application to AWS Fargate with Amazon Q: A Journey of "No-Touch Coding"

The Initial Approach: BDD with Amazon Q

I started with BDD, crafting Gherkin-style specifications:

Feature: Expense Tracking
  As a user
  I want to record my expenses
  So that I can track my spending

  Scenario: Add a new expense
    Given I have the expense details
    When I add a new expense with amount "45.99", category "Groceries", and description "Weekly shopping"
    Then the expense should be saved with the current date
    And the total expenses should increase by "45.99"

This approach was excellent for defining user-centric requirements. Amazon Q understood the high-level functionality I wanted to build. However, when it came to generating the actual implementation, something was missing.

The Turning Point: Discovering TDD's Power

When I switched to TDD, providing Amazon Q with test cases instead, something remarkable happened:

def test_add_expense(self):
    tracker = BudgetTracker()
    expense = tracker.add_expense("45.99", "Groceries", datetime.now(), "Weekly shopping")
    self.assertEqual(expense.amount, Decimal("45.99"))
    self.assertEqual(expense.category, "Groceries")
    self.assertEqual(expense.description, "Weekly shopping")
    self.assertEqual(len(tracker.expenses), 1)

def test_get_balance(self):
    tracker = BudgetTracker()
    tracker.add_expense("45.99", "Groceries", datetime.now(), "Weekly shopping")
    tracker.add_expense("20.00", "Entertainment", datetime.now(), "Movie tickets")
    self.assertEqual(tracker.get_balance(), Decimal("65.99"))

The code Amazon Q generated was significantly more precise, aligned perfectly with my needs, and required fewer iterations to get right.

Why TDD Works Better with LLM-Based Tools

1. Concrete Implementation Details

BDD focuses on behavior from a user's perspective, which is valuable but abstract. TDD, on the other hand, provides concrete implementation details - method names, parameters, return types, and assertions. For our Budget Tracker application, the test cases clearly defined the BudgetTracker class structure, the add_expense method signature, and the expected behavior of the get_balance method.

This level of detail gives Amazon Q's underlying LLM much more context about what you're trying to build. It's like giving precise measurements to a carpenter instead of just describing the furniture you want.

2. Executable Specifications

While BDD provides natural language specifications, TDD provides executable specifications. When I provided Amazon Q with test cases for filtering expenses by date range:

def test_filter_by_date_range(self):
    tracker = BudgetTracker()
    date1 = datetime(2025, 1, 1)
    date2 = datetime(2025, 1, 15)
    date3 = datetime(2025, 1, 30)
    tracker.add_expense("10.00", "Food", date1, "Lunch")
    tracker.add_expense("20.00", "Food", date2, "Dinner")
    tracker.add_expense("30.00", "Food", date3, "Groceries")

    filtered = tracker.filter_by_date_range(datetime(2025, 1, 10), datetime(2025, 1, 20))
    self.assertEqual(len(filtered), 1)
    self.assertEqual(filtered[0].amount, Decimal("20.00"))

Amazon Q immediately understood not just that I wanted date filtering, but exactly how the filtering should work, what edge cases to handle, and how to structure the method.

3. Precision in Error Handling

TDD excels at defining how your code should handle edge cases and errors. When developing the Budget Tracker's validation logic, I included tests for invalid inputs:

def test_invalid_amount(self):
    tracker = BudgetTracker()
    with self.assertRaises(ValueError):
        tracker.add_expense("invalid", "Groceries", datetime.now(), "Weekly shopping")

This simple test case gave Amazon Q crucial information about how to handle validation, resulting in robust error handling in the generated code:

def add_expense(self, amount: str, category: str, date: datetime, description: str = "") -> Expense:
    try:
        decimal_amount = Decimal(amount)
    except (ValueError, DecimalException):
        raise ValueError("Amount must be a valid decimal number")

    # Rest of the implementation...

4. Iterative Development Path

When building the Flask API layer of our Budget Tracker, TDD provided a clear path for iterative development. Each test defined a specific endpoint and its expected behavior:

def test_add_expense_endpoint(self):
    response = self.client.post('/expense', json={
        'amount': '45.99',
        'category': 'Groceries',
        'date': '2025-04-02',
        'description': 'Weekly grocery shopping'
    })
    self.assertEqual(response.status_code, 201)
    data = json.loads(response.data)
    self.assertIn('id', data)

Amazon Q used these tests to generate not just the endpoint implementation, but also proper request validation, error handling, and response formatting - all aligned with the test expectations.

The Hybrid Approach: Getting the Best of Both Worlds

While TDD proved more effective with Amazon Q, I didn't abandon BDD entirely. Instead, I adopted a hybrid approach:

  1. Start with BDD to define high-level requirements and user stories

  2. Translate these into TDD test cases to provide detailed implementation guidance

  3. Use Amazon Q to generate code based on the test cases

  4. Iterate on the tests and implementation as needed

This approach gave Amazon Q both the "what" (from BDD) and the "how" (from TDD), resulting in higher quality code with fewer iterations.

Conclusion: TDD as the Secret Weapon for LLM-Powered Development

When working with LLM-based coding assistants like Amazon Q Developer, Test-Driven Development provides the rich, detailed context that these models need to generate precise, high-quality code. While BDD helps define what your application should do from a user's perspective, TDD tells the AI exactly how it should be implemented.

In our Budget Tracker project, this approach led to:

  • Fewer iterations to get the code right

  • More precise implementation of features

  • Better handling of edge cases and errors

  • A clearer development path from core functionality to API endpoints

For developers looking to maximize the effectiveness of AI coding assistants, starting with well-defined test cases might be the most powerful approach you can take. The tests not only guide the AI but also serve as documentation and validation for the generated code - a win-win for development efficiency and code quality.

As LLM-based development tools continue to evolve, the developers who can provide the most precise context through methodologies like TDD will be the ones who harness their full potential.