You've inherited code without tests, and you need to add tests before making changes.

This guide walks you through adding tests to existing code step-by-step. You'll learn to identify what to test first, write initial tests safely, and gradually improve coverage.

Before starting, read [Fundamentals of Software Testing]({{< ref "fundamentals-of-software-testing" >}}) to understand testing principles.

## Prerequisites

Before adding tests to existing code, ensure you have:

* **A codebase without tests** - Or with insufficient test coverage
* **A testing framework installed** - pytest (Python), Jest (JavaScript), JUnit (Java), etc.
* **Ability to run the code** - You need to understand what the code currently does
* **Permission to make changes** - Ensure you can modify the codebase

## The Example: E-commerce Order Calculator

We'll work with a real-world example: an order calculator that computes totals, applies discounts, and calculates tax.

**The existing code** (untested, production code):

```python
# order_calculator.py
class OrderCalculator:
    def __init__(self):
        self.tax_rate = 0.08  # 8% tax
        self.discount_threshold = 100.00

    def calculate_total(self, items, customer_type):
        """Calculate order total with discounts and tax."""
        subtotal = 0

        # Sum item prices
        for item in items:
            subtotal += item['price'] * item['quantity']

        # Apply discount for large orders
        if subtotal >= self.discount_threshold:
            if customer_type == 'premium':
                subtotal = subtotal * 0.85  # 15% off
            elif customer_type == 'regular':
                subtotal = subtotal * 0.95  # 5% off

        # Add tax
        tax = subtotal * self.tax_rate
        total = subtotal + tax

        return total

    def apply_coupon(self, total, coupon_code):
        """Apply coupon code to total."""
        if coupon_code == 'SAVE10':
            return total - 10.00
        elif coupon_code == 'SAVE20':
            return total - 20.00
        else:
            return total

    def calculate_shipping(self, total, shipping_method):
        """Calculate shipping cost."""
        if shipping_method == 'express':
            return 20.00
        elif shipping_method == 'standard':
            if total > 50:
                return 0  # Free shipping over $50
            else:
                return 5.00
        else:
            return 0
```

This code works in production, but has no tests. Let's add them systematically.

## Step 1: Understand Current Behavior

Before adding tests, run the code to understand what it currently does.

**Create a simple script to exercise the code:**

```python
# manual_test.py
from order_calculator import OrderCalculator

calc = OrderCalculator()

# Test case 1: Regular customer, small order
items = [
    {'price': 20.00, 'quantity': 2},  # $40
    {'price': 15.00, 'quantity': 1},  # $15
]
total = calc.calculate_total(items, 'regular')
print(f"Small order, regular customer: ${total:.2f}")

# Test case 2: Premium customer, large order
items = [
    {'price': 50.00, 'quantity': 3},  # $150
]
total = calc.calculate_total(items, 'premium')
print(f"Large order, premium customer: ${total:.2f}")

# Test case 3: Apply coupon
total_with_coupon = calc.apply_coupon(100.00, 'SAVE10')
print(f"After SAVE10 coupon: ${total_with_coupon:.2f}")

# Test case 4: Shipping
shipping_express = calc.calculate_shipping(100.00, 'express')
shipping_standard = calc.calculate_shipping(60.00, 'standard')
print(f"Express shipping: ${shipping_express:.2f}")
print(f"Standard shipping (over $50): ${shipping_standard:.2f}")
```

**Run the manual test:**

```bash
python manual_test.py
```

**Expected output:**

```text
Small order, regular customer: $59.40
Large order, premium customer: $137.70
After SAVE10 coupon: $90.00
Express shipping: $20.00
Standard shipping (over $50): $0.00
```

**Why this matters:** Understanding current behavior ensures your tests verify what the code actually does, not what you think it should do.

## Step 2: Identify What to Test First

Don't try to test everything at once. Prioritize based on risk and value.

**Ask yourself:**

1. **What's most critical?** - Code that handles money, security, or user data
2. **What's most complex?** - Functions with multiple branches and edge cases
3. **What changes most often?** - Code you're about to modify
4. **What breaks most often?** - Code with known bugs

**For our example, prioritize:**

1. ✅ **`calculate_total`** - Handles money, complex logic, critical
2. ✅ **`apply_coupon`** - Handles money, moderate complexity
3. ✅ **`calculate_shipping`** - Important but simpler

**Start with `calculate_total` because it's the most critical and complex.**

## Step 3: Write Your First Test

Start with the simplest happy path test.

**Create test file:**

```python
# test_order_calculator.py
import pytest
from order_calculator import OrderCalculator

def test_calculate_total_for_small_order_regular_customer():
    """Test that small order total is calculated correctly for regular customer."""
    # Arrange
    calc = OrderCalculator()
    items = [
        {'price': 20.00, 'quantity': 2},  # $40
        {'price': 15.00, 'quantity': 1},  # $15
    ]
    # Subtotal: $55 (no discount, below threshold)
    # Tax: $55 * 0.08 = $4.40
    # Total: $59.40

    # Act
    total = calc.calculate_total(items, 'regular')

    # Assert
    assert total == 59.40
```

**Run the test:**

```bash
pytest test_order_calculator.py -v
```

**Expected output:**

```text
PASSED test_order_calculator.py::test_calculate_total_for_small_order_regular_customer
```

**Congratulations!** You've written your first test for existing code.

## Step 4: Add More Test Cases Systematically

Now add tests for different scenarios, one at a time.

### Test 2: Large Order with Discount

```python
def test_calculate_total_for_large_order_premium_customer():
    """Test that large order gets premium discount and tax applied."""
    # Arrange
    calc = OrderCalculator()
    items = [
        {'price': 50.00, 'quantity': 3},  # $150
    ]
    # Subtotal: $150
    # After 15% discount: $150 * 0.85 = $127.50
    # Tax: $127.50 * 0.08 = $10.20
    # Total: $137.70

    # Act
    total = calc.calculate_total(items, 'premium')

    # Assert
    assert total == 137.70
```

**Run tests:**

```bash
pytest test_order_calculator.py -v
```

**Expected output:**

```text
PASSED test_order_calculator.py::test_calculate_total_for_small_order_regular_customer
PASSED test_order_calculator.py::test_calculate_total_for_large_order_premium_customer
```

### Test 3: Edge Case - Empty Order

```python
def test_calculate_total_for_empty_order():
    """Test that empty order returns zero."""
    # Arrange
    calc = OrderCalculator()
    items = []

    # Act
    total = calc.calculate_total(items, 'regular')

    # Assert
    assert total == 0.00
```

**Run the test:**

```bash
pytest test_order_calculator.py::test_calculate_total_for_empty_order -v
```

**Expected output:**

```text
PASSED test_order_calculator.py::test_calculate_total_for_empty_order
```

### Test 4: Boundary Condition - Exactly at Threshold

```python
def test_calculate_total_at_discount_threshold():
    """Test that order exactly at threshold gets discount."""
    # Arrange
    calc = OrderCalculator()
    items = [
        {'price': 100.00, 'quantity': 1},  # Exactly $100
    ]
    # Subtotal: $100
    # After 5% discount (regular): $100 * 0.95 = $95
    # Tax: $95 * 0.08 = $7.60
    # Total: $102.60

    # Act
    total = calc.calculate_total(items, 'regular')

    # Assert
    assert total == 102.60
```

### Test 5: Boundary Condition - Just Below Threshold

```python
def test_calculate_total_below_discount_threshold():
    """Test that order just below threshold gets no discount."""
    # Arrange
    calc = OrderCalculator()
    items = [
        {'price': 99.99, 'quantity': 1},  # Just under $100
    ]
    # Subtotal: $99.99 (no discount)
    # Tax: $99.99 * 0.08 = $7.9992, rounded to $7.999
    # Total: $107.99 (approximately)

    # Act
    total = calc.calculate_total(items, 'regular')

    # Assert
    assert total == pytest.approx(107.99, rel=0.01)  # Allow small rounding difference
```

**Run all tests:**

```bash
pytest test_order_calculator.py -v
```

**Expected output:**

```text
PASSED test_order_calculator.py::test_calculate_total_for_small_order_regular_customer
PASSED test_order_calculator.py::test_calculate_total_for_large_order_premium_customer
PASSED test_order_calculator.py::test_calculate_total_for_empty_order
PASSED test_order_calculator.py::test_calculate_total_at_discount_threshold
PASSED test_order_calculator.py::test_calculate_total_below_discount_threshold
```

## Step 5: Test Other Functions

Now that `calculate_total` has solid coverage, test the other functions.

### Testing `apply_coupon`

```python
def test_apply_coupon_save10():
    """Test that SAVE10 coupon subtracts $10."""
    # Arrange
    calc = OrderCalculator()
    total = 100.00

    # Act
    discounted = calc.apply_coupon(total, 'SAVE10')

    # Assert
    assert discounted == 90.00

def test_apply_coupon_save20():
    """Test that SAVE20 coupon subtracts $20."""
    calc = OrderCalculator()
    total = 100.00
    discounted = calc.apply_coupon(total, 'SAVE20')
    assert discounted == 80.00

def test_apply_coupon_invalid_code():
    """Test that invalid coupon code doesn't change total."""
    calc = OrderCalculator()
    total = 100.00
    discounted = calc.apply_coupon(total, 'INVALID')
    assert discounted == 100.00
```

### Testing `calculate_shipping`

```python
def test_calculate_shipping_express():
    """Test that express shipping costs $20."""
    calc = OrderCalculator()
    shipping = calc.calculate_shipping(100.00, 'express')
    assert shipping == 20.00

def test_calculate_shipping_standard_over_threshold():
    """Test that standard shipping is free over $50."""
    calc = OrderCalculator()
    shipping = calc.calculate_shipping(60.00, 'standard')
    assert shipping == 0.00

def test_calculate_shipping_standard_under_threshold():
    """Test that standard shipping costs $5 under $50."""
    calc = OrderCalculator()
    shipping = calc.calculate_shipping(40.00, 'standard')
    assert shipping == 5.00

def test_calculate_shipping_unknown_method():
    """Test that unknown shipping method returns $0."""
    calc = OrderCalculator()
    shipping = calc.calculate_shipping(100.00, 'unknown')
    assert shipping == 0.00
```

**Run all tests:**

```bash
pytest test_order_calculator.py -v
```

**You should now have 13 passing tests!**

## Step 6: Check Coverage

See what code is tested and what's not.

**Install coverage tool:**

```bash
pip install pytest-cov
```

**Run tests with coverage:**

```bash
pytest test_order_calculator.py --cov=order_calculator --cov-report=term-missing
```

**Expected output:**

```text
---------- coverage: platform linux, python 3.x -----------
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
order_calculator.py        30      0   100%
-----------------------------------------------------
TOTAL                      30      0   100%
```

**Congratulations!** You've achieved 100% coverage of the `OrderCalculator` class.

## Step 7: Find and Fix a Bug with Tests

Now that you have tests, you can safely find and fix bugs.

**Discovered bug:** What happens if someone passes a negative quantity?

```python
def test_calculate_total_with_negative_quantity():
    """Test that negative quantity is handled."""
    calc = OrderCalculator()
    items = [
        {'price': 20.00, 'quantity': -1},  # Negative quantity
    ]

    # Current behavior: allows negative total (bug!)
    total = calc.calculate_total(items, 'regular')

    # This test will fail because code doesn't validate
    assert total > 0  # We expect an error or zero, not negative
```

**Run the test:**

```bash
pytest test_order_calculator.py::test_calculate_total_with_negative_quantity -v
```

**Expected output:**

```text
FAILED test_order_calculator.py::test_calculate_total_with_negative_quantity
AssertionError: assert -21.6 > 0
```

**The test caught a bug!** The code allows negative quantities, resulting in negative totals.

**Fix the bug:**

```python
# order_calculator.py
def calculate_total(self, items, customer_type):
    """Calculate order total with discounts and tax."""
    subtotal = 0

    # Sum item prices
    for item in items:
        # Validate quantity
        if item['quantity'] < 0:
            raise ValueError(f"Quantity cannot be negative: {item['quantity']}")

        subtotal += item['price'] * item['quantity']

    # ... rest of function
```

**Update the test to expect the error:**

```python
def test_calculate_total_with_negative_quantity_raises_error():
    """Test that negative quantity raises ValueError."""
    calc = OrderCalculator()
    items = [
        {'price': 20.00, 'quantity': -1},
    ]

    with pytest.raises(ValueError, match="Quantity cannot be negative"):
        calc.calculate_total(items, 'regular')
```

**Run the test:**

```bash
pytest test_order_calculator.py::test_calculate_total_with_negative_quantity_raises_error -v
```

**Expected output:**

```text
PASSED test_order_calculator.py::test_calculate_total_with_negative_quantity_raises_error
```

**The test now passes, and the bug is fixed!**

## Step 8: Refactor with Confidence

Now that you have tests, you can safely refactor the code.

**Before refactoring:**

```python
def calculate_total(self, items, customer_type):
    """Calculate order total with discounts and tax."""
    subtotal = 0

    for item in items:
        if item['quantity'] < 0:
            raise ValueError(f"Quantity cannot be negative: {item['quantity']}")
        subtotal += item['price'] * item['quantity']

    if subtotal >= self.discount_threshold:
        if customer_type == 'premium':
            subtotal = subtotal * 0.85
        elif customer_type == 'regular':
            subtotal = subtotal * 0.95

    tax = subtotal * self.tax_rate
    total = subtotal + tax

    return total
```

**After refactoring** (extracting methods for clarity):

```python
def _calculate_subtotal(self, items):
    """Calculate subtotal from items."""
    subtotal = 0
    for item in items:
        if item['quantity'] < 0:
            raise ValueError(f"Quantity cannot be negative: {item['quantity']}")
        subtotal += item['price'] * item['quantity']
    return subtotal

def _apply_discount(self, subtotal, customer_type):
    """Apply customer discount to subtotal."""
    if subtotal < self.discount_threshold:
        return subtotal

    if customer_type == 'premium':
        return subtotal * 0.85  # 15% off
    elif customer_type == 'regular':
        return subtotal * 0.95  # 5% off
    else:
        return subtotal

def calculate_total(self, items, customer_type):
    """Calculate order total with discounts and tax."""
    subtotal = self._calculate_subtotal(items)
    discounted = self._apply_discount(subtotal, customer_type)
    tax = discounted * self.tax_rate
    return discounted + tax
```

**Run all tests to verify refactoring didn't break anything:**

```bash
pytest test_order_calculator.py -v
```

**Expected output:**

```text
All tests pass!
```

**The refactoring is safe because tests verify behavior is unchanged.**

## Step 9: Maintain Tests as Code Evolves

As you add features, add tests at the same time.

**New requirement:** Add a "bulk" customer type with 20% discount.

**Write the test first:**

```python
def test_calculate_total_for_bulk_customer():
    """Test that bulk customers get 20% discount."""
    calc = OrderCalculator()
    items = [
        {'price': 100.00, 'quantity': 2},  # $200
    ]
    # Subtotal: $200
    # After 20% discount: $200 * 0.80 = $160
    # Tax: $160 * 0.08 = $12.80
    # Total: $172.80

    total = calc.calculate_total(items, 'bulk')
    assert total == 172.80
```

**Run the test (it will fail):**

```bash
pytest test_order_calculator.py::test_calculate_total_for_bulk_customer -v
```

**Now implement the feature:**

```python
def _apply_discount(self, subtotal, customer_type):
    """Apply customer discount to subtotal."""
    if subtotal < self.discount_threshold:
        return subtotal

    if customer_type == 'premium':
        return subtotal * 0.85  # 15% off
    elif customer_type == 'bulk':
        return subtotal * 0.80  # 20% off
    elif customer_type == 'regular':
        return subtotal * 0.95  # 5% off
    else:
        return subtotal
```

**Run the test (it should pass now):**

```bash
pytest test_order_calculator.py::test_calculate_total_for_bulk_customer -v
```

**You've successfully added a feature with tests!**

## Common Challenges and Solutions

### Challenge 1: Code is Hard to Test

**Problem:** Functions have many dependencies, making tests difficult.

**Solution:** Use dependency injection and mocks.

**Before:**

```python
def process_order(order_id):
    db = connect_to_database()  # Hard to test
    order = db.get_order(order_id)
    # ...
```

**After:**

```python
def process_order(order_id, db):  # Dependency injected
    order = db.get_order(order_id)
    # ...
```

**Test:**

```python
def test_process_order():
    mock_db = Mock()
    mock_db.get_order.return_value = {'id': 1, 'items': []}
    process_order(1, mock_db)
    mock_db.get_order.assert_called_once_with(1)
```

### Challenge 2: Can't Change Production Code

**Problem:** Code is in production, and you can't risk breaking it.

**Solution:** Characterization tests - tests that document current behavior, even if it's wrong.

```python
def test_current_behavior_may_be_wrong():
    """Document current behavior, even if it seems wrong."""
    # TODO: This behavior seems incorrect, but documenting it
    # so we can safely refactor later
    result = legacy_function(weird_input)
    assert result == weird_output  # Whatever it currently returns
```

### Challenge 3: No Idea What Code Does

**Problem:** Code is complex, and you don't understand it.

**Solution:** Write exploratory tests to learn.

```python
def test_explore_function_behavior():
    """Explore what this function actually does."""
    # Try different inputs and see what happens
    result1 = mystery_function(0)
    result2 = mystery_function(1)
    result3 = mystery_function(-1)

    # Document what you learned
    assert result1 == expected1  # Fill in after running
    assert result2 == expected2
    assert result3 == expected3
```

### Challenge 4: Tests Are Too Slow

**Problem:** Tests take too long because they hit databases or external services.

**Solution:** Use mocks and test doubles.

**Before:**

```python
def test_user_creation():
    db = RealDatabase()  # Slow
    user = create_user('test@example.com', db)
    assert user.email == 'test@example.com'
```

**After:**

```python
def test_user_creation():
    mock_db = Mock()  # Fast
    user = create_user('test@example.com', mock_db)
    assert user.email == 'test@example.com'
    mock_db.save.assert_called_once()
```

## Next Steps

You've successfully added tests to existing code! Here's what to do next:

1. **Continue adding tests** - Work through the rest of your codebase, prioritizing critical code
2. **Set up continuous integration** - Run tests automatically on every commit
3. **Establish coverage goals** - Aim for high coverage of critical paths
4. **Review testing practices** - Use the [Reference: Testing Checklist]({{< ref "reference-testing-checklist" >}})
5. **Learn advanced techniques** - Study mocking, fixtures, and test organization

## Summary

You've learned to:

* ✅ Understand existing code behavior before testing
* ✅ Identify what to test first (critical, complex, changing code)
* ✅ Write your first test for existing code
* ✅ Add tests systematically (happy path, edge cases, errors)
* ✅ Check test coverage
* ✅ Find and fix bugs with tests
* ✅ Refactor safely with test protection
* ✅ Maintain tests as code evolves

## Key Takeaways

**When adding tests to existing code:**

1. **Start small** - One function, one test at a time
2. **Prioritize** - Test critical code first
3. **Document current behavior** - Tests show what code does today
4. **Find bugs safely** - Tests catch regressions as you improve code
5. **Refactor confidently** - Tests verify behavior is preserved

**Remember:** You don't need 100% coverage immediately. Start with critical paths and grow coverage over time.

## References

* [Fundamentals of Software Testing]({{< ref "fundamentals-of-software-testing" >}}) - Testing principles and concepts
* [How to Write Effective Unit Tests]({{< ref "how-to-write-effective-unit-tests" >}}) - Step-by-step unit testing workflow
* [Reference: Testing Checklist]({{< ref "reference-testing-checklist" >}}) - Comprehensive testing checklist
* Michael Feathers' "Working Effectively with Legacy Code" - The definitive guide to adding tests to existing code
* Martin Fowler's "Refactoring" - Techniques for improving code with test protection

You now have a systematic approach to adding tests to existing code. Start with one function, write one test, and build from there.