Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on regression testing! ๐ In this guide, weโll explore how to prevent bugs from sneaking back into your code after youโve fixed them.
Have you ever fixed a bug, only to find it mysteriously reappear weeks later? ๐ฑ Thatโs where regression testing comes to the rescue! Whether youโre building web applications ๐, data pipelines ๐, or game engines ๐ฎ, understanding regression testing is essential for maintaining stable, reliable software.
By the end of this tutorial, youโll feel confident implementing regression tests that catch bugs before they reach your users! Letโs dive in! ๐โโ๏ธ
๐ Understanding Regression Testing
๐ค What is Regression Testing?
Regression testing is like having a time machine for bugs ๐ฐ๏ธ. Think of it as a safety net that catches old bugs trying to sneak back into your code when you make changes.
In Python terms, regression testing ensures that new code changes donโt break existing functionality. This means you can:
- โจ Add new features without fear
- ๐ Refactor code with confidence
- ๐ก๏ธ Catch bugs before users do
๐ก Why Use Regression Testing?
Hereโs why developers love regression testing:
- Confidence in Changes ๐: Make updates without breaking existing features
- Early Bug Detection ๐: Catch issues during development, not production
- Living Documentation ๐: Tests document expected behavior
- Refactoring Safety ๐ง: Restructure code fearlessly
Real-world example: Imagine youโre building an e-commerce site ๐. You fix a checkout bug, but later when adding a discount feature, the checkout breaks again! With regression tests, youโd catch this immediately.
๐ง Basic Syntax and Usage
๐ Simple Example with pytest
Letโs start with a friendly example using pytest:
# ๐ Hello, Regression Testing!
# calculator.py
class Calculator:
def add(self, a, b):
return a + b # ๐ข Simple addition
def subtract(self, a, b):
return a - b # โ Subtraction
def multiply(self, a, b):
return a * b # โ๏ธ Multiplication
# test_calculator.py
import pytest
from calculator import Calculator
class TestCalculator:
def setup_method(self):
self.calc = Calculator() # ๐จ Create fresh calculator
def test_addition(self):
# ๐งช Test basic addition
assert self.calc.add(2, 3) == 5
assert self.calc.add(-1, 1) == 0
assert self.calc.add(0, 0) == 0
def test_subtraction(self):
# โ Test subtraction edge cases
assert self.calc.subtract(5, 3) == 2
assert self.calc.subtract(0, 5) == -5
def test_multiplication(self):
# โจ Test multiplication scenarios
assert self.calc.multiply(3, 4) == 12
assert self.calc.multiply(0, 100) == 0
assert self.calc.multiply(-2, 3) == -6
๐ก Explanation: Notice how we test various scenarios! Each test is a guard against future bugs. When you modify the calculator, these tests ensure nothing breaks! ๐ก๏ธ
๐ฏ Creating a Regression Test Suite
Hereโs how to build a comprehensive test suite:
# ๐๏ธ shopping_cart.py
class ShoppingCart:
def __init__(self):
self.items = [] # ๐ Empty cart
self.discount = 0 # ๐ฐ No discount initially
def add_item(self, item, price, quantity=1):
# โ Add items to cart
self.items.append({
'name': item,
'price': price,
'quantity': quantity,
'emoji': '๐ฆ'
})
def get_total(self):
# ๐ต Calculate total with discount
subtotal = sum(item['price'] * item['quantity']
for item in self.items)
return subtotal * (1 - self.discount)
def apply_discount(self, discount_percent):
# ๐ Apply discount (0-100%)
self.discount = discount_percent / 100
# ๐งช test_shopping_cart_regression.py
import pytest
from shopping_cart import ShoppingCart
class TestShoppingCartRegression:
def setup_method(self):
self.cart = ShoppingCart()
def test_empty_cart_total(self):
# ๐ Regression: Empty cart should be 0
assert self.cart.get_total() == 0
def test_single_item_addition(self):
# ๐ฆ Regression: Single item calculation
self.cart.add_item("Python Book", 29.99)
assert self.cart.get_total() == 29.99
def test_multiple_items_total(self):
# ๐ Regression: Multiple items
self.cart.add_item("Coffee", 4.99, 2)
self.cart.add_item("Cookies", 3.50, 3)
expected = (4.99 * 2) + (3.50 * 3)
assert self.cart.get_total() == expected
def test_discount_calculation(self):
# ๐ฐ Regression: Discount should work correctly
self.cart.add_item("Laptop", 999.99)
self.cart.apply_discount(10) # 10% off
assert self.cart.get_total() == 899.991
def test_discount_with_multiple_items(self):
# ๐ Regression: Discount on multiple items
self.cart.add_item("Mouse", 25.00)
self.cart.add_item("Keyboard", 75.00)
self.cart.apply_discount(20) # 20% off
assert self.cart.get_total() == 80.0
๐ก Practical Examples
๐ Example 1: E-commerce Price Calculator
Letโs build a real-world regression test suite:
# ๐ณ price_calculator.py
class PriceCalculator:
def __init__(self):
self.tax_rate = 0.08 # 8% tax
self.shipping_rates = {
'standard': 5.99,
'express': 12.99,
'overnight': 24.99
}
def calculate_subtotal(self, items):
# ๐งฎ Calculate items subtotal
return sum(item['price'] * item['quantity']
for item in items)
def calculate_tax(self, subtotal):
# ๐ธ Calculate tax amount
return round(subtotal * self.tax_rate, 2)
def calculate_shipping(self, subtotal, shipping_type='standard'):
# ๐ฆ Free shipping over $50!
if subtotal >= 50:
return 0
return self.shipping_rates.get(shipping_type, 5.99)
def calculate_total(self, items, shipping_type='standard',
coupon_code=None):
# ๐ฐ Calculate final total
subtotal = self.calculate_subtotal(items)
# ๐๏ธ Apply coupon if valid
if coupon_code == "SAVE10":
subtotal *= 0.9 # 10% off
elif coupon_code == "SAVE20":
subtotal *= 0.8 # 20% off
tax = self.calculate_tax(subtotal)
shipping = self.calculate_shipping(subtotal, shipping_type)
return round(subtotal + tax + shipping, 2)
# ๐งช test_price_calculator_regression.py
import pytest
from price_calculator import PriceCalculator
class TestPriceCalculatorRegression:
@pytest.fixture
def calculator(self):
return PriceCalculator()
@pytest.fixture
def sample_items(self):
return [
{'name': 'T-shirt', 'price': 19.99, 'quantity': 2},
{'name': 'Jeans', 'price': 49.99, 'quantity': 1}
]
def test_subtotal_calculation(self, calculator, sample_items):
# ๐งฎ Regression: Subtotal math must be correct
expected = (19.99 * 2) + 49.99
assert calculator.calculate_subtotal(sample_items) == expected
def test_tax_calculation(self, calculator):
# ๐ธ Regression: Tax should be 8%
assert calculator.calculate_tax(100) == 8.0
assert calculator.calculate_tax(49.99) == 4.0
def test_free_shipping_threshold(self, calculator):
# ๐ฆ Regression: Free shipping over $50
assert calculator.calculate_shipping(49.99) == 5.99
assert calculator.calculate_shipping(50.00) == 0
assert calculator.calculate_shipping(100.00) == 0
def test_express_shipping_under_threshold(self, calculator):
# ๐ Regression: Express shipping charges
assert calculator.calculate_shipping(30, 'express') == 12.99
def test_total_with_coupon(self, calculator, sample_items):
# ๐๏ธ Regression: Coupon codes work correctly
# Without coupon
total_no_coupon = calculator.calculate_total(sample_items)
# With SAVE10 coupon
total_save10 = calculator.calculate_total(
sample_items, coupon_code="SAVE10"
)
# With SAVE20 coupon
total_save20 = calculator.calculate_total(
sample_items, coupon_code="SAVE20"
)
# Verify discounts applied correctly
assert total_save10 < total_no_coupon
assert total_save20 < total_save10
def test_bug_fix_negative_quantity(self, calculator):
# ๐ Regression: Fixed bug where negative quantities broke total
items = [{'name': 'Item', 'price': 10, 'quantity': -1}]
# This used to cause issues, now should handle gracefully
result = calculator.calculate_subtotal(items)
assert result == -10 # Or could validate and raise error
๐ฏ Try it yourself: Add a test for bulk discount (5% off orders over $200)!
๐ฎ Example 2: Game Score System
Letโs test a game scoring system:
# ๐ game_scorer.py
class GameScorer:
def __init__(self):
self.scores = {}
self.achievements = {}
self.multipliers = {
'combo': 1.5,
'perfect': 2.0,
'speed_bonus': 1.2
}
def add_player(self, player_name):
# ๐ฎ Initialize player
self.scores[player_name] = 0
self.achievements[player_name] = []
def add_score(self, player_name, points, bonus_type=None):
# ๐ฏ Add score with optional bonus
if player_name not in self.scores:
raise ValueError(f"Player {player_name} not found! ๐ฑ")
actual_points = points
if bonus_type and bonus_type in self.multipliers:
actual_points = int(points * self.multipliers[bonus_type])
self.scores[player_name] += actual_points
# ๐ Check for achievements
self._check_achievements(player_name)
return actual_points
def _check_achievements(self, player_name):
# ๐ Award achievements based on score
score = self.scores[player_name]
achievements = self.achievements[player_name]
if score >= 100 and "Rookie" not in achievements:
achievements.append("Rookie ๐")
if score >= 500 and "Pro" not in achievements:
achievements.append("Pro ๐")
if score >= 1000 and "Master" not in achievements:
achievements.append("Master ๐")
def get_leaderboard(self):
# ๐ Get sorted leaderboard
return sorted(self.scores.items(),
key=lambda x: x[1], reverse=True)
# ๐งช test_game_scorer_regression.py
import pytest
from game_scorer import GameScorer
class TestGameScorerRegression:
@pytest.fixture
def game(self):
return GameScorer()
def test_player_initialization(self, game):
# ๐ค Regression: New players start at 0
game.add_player("Alice")
assert game.scores["Alice"] == 0
assert game.achievements["Alice"] == []
def test_basic_scoring(self, game):
# ๐ฏ Regression: Basic score addition
game.add_player("Bob")
game.add_score("Bob", 50)
game.add_score("Bob", 30)
assert game.scores["Bob"] == 80
def test_combo_multiplier(self, game):
# ๐ฅ Regression: Combo bonus calculation
game.add_player("Charlie")
points_added = game.add_score("Charlie", 100, "combo")
assert points_added == 150 # 100 * 1.5
assert game.scores["Charlie"] == 150
def test_achievement_unlocking(self, game):
# ๐ Regression: Achievements unlock at right scores
game.add_player("Diana")
# Score 99 - no achievement
game.add_score("Diana", 99)
assert "Rookie ๐" not in game.achievements["Diana"]
# Score 100 - unlock Rookie
game.add_score("Diana", 1)
assert "Rookie ๐" in game.achievements["Diana"]
# Score 500 - unlock Pro
game.add_score("Diana", 400)
assert "Pro ๐" in game.achievements["Diana"]
def test_leaderboard_sorting(self, game):
# ๐ Regression: Leaderboard sorts correctly
game.add_player("Player1")
game.add_player("Player2")
game.add_player("Player3")
game.add_score("Player1", 300)
game.add_score("Player2", 500)
game.add_score("Player3", 200)
leaderboard = game.get_leaderboard()
assert leaderboard[0][0] == "Player2" # Highest score
assert leaderboard[1][0] == "Player1"
assert leaderboard[2][0] == "Player3" # Lowest score
def test_nonexistent_player_error(self, game):
# โ Regression: Error handling for unknown players
with pytest.raises(ValueError) as exc_info:
game.add_score("Ghost", 100)
assert "not found" in str(exc_info.value)
๐ Advanced Concepts
๐งโโ๏ธ Parameterized Regression Tests
When youโre ready to level up, use parameterized tests:
# ๐ฏ Advanced parameterized testing
import pytest
class TestAdvancedRegression:
@pytest.mark.parametrize("input_value,expected", [
(0, 0),
(1, 1),
(5, 120), # 5! = 120
(10, 3628800), # 10! = 3628800
])
def test_factorial_regression(self, input_value, expected):
# ๐งฎ Test factorial with multiple cases
from math import factorial
assert factorial(input_value) == expected
@pytest.mark.parametrize("test_input,expected_type", [
("123", int),
("12.34", float),
("true", bool),
("hello", str),
("[1,2,3]", list),
])
def test_type_converter_regression(self, test_input, expected_type):
# ๐ Test type conversion regression
from ast import literal_eval
try:
result = literal_eval(test_input)
assert isinstance(result, expected_type)
except:
# String fallback
assert expected_type == str
๐๏ธ Regression Test Fixtures
For the brave developers, use fixtures for complex setups:
# ๐ Advanced fixture-based regression testing
import pytest
import tempfile
import json
class TestDatabaseRegression:
@pytest.fixture
def test_database(self):
# ๐๏ธ Create temporary test database
with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as f:
test_data = {
"users": [
{"id": 1, "name": "Alice", "emoji": "๐ฉโ๐ป"},
{"id": 2, "name": "Bob", "emoji": "๐จโ๐ป"}
],
"products": [
{"id": 1, "name": "Python Book", "price": 29.99}
]
}
json.dump(test_data, f)
f.flush()
yield f.name
@pytest.fixture
def mock_api_responses(self):
# ๐ Mock API responses for testing
return {
"/users": [{"id": 1, "name": "Test User"}],
"/products": [{"id": 1, "price": 99.99}],
"/orders": []
}
def test_database_read_regression(self, test_database):
# ๐ Regression: Database reading works
with open(test_database) as f:
data = json.load(f)
assert len(data["users"]) == 2
assert data["users"][0]["emoji"] == "๐ฉโ๐ป"
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Fragile Tests
# โ Wrong way - test depends on exact string format
def test_user_greeting_bad():
user = User("John")
# This breaks if we change punctuation or spacing!
assert user.greet() == "Hello, John! Welcome to our app."
# โ
Correct way - test the important parts
def test_user_greeting_good():
user = User("John")
greeting = user.greet()
assert "John" in greeting # ๐ค Name is included
assert "welcome" in greeting.lower() # ๐ Welcome message exists
๐คฏ Pitfall 2: Not Testing Edge Cases
# โ Dangerous - only testing happy path
def test_divide_bad():
assert divide(10, 2) == 5
# โ
Safe - test edge cases for regression
def test_divide_good():
# ๐ฏ Normal case
assert divide(10, 2) == 5
# ๐ฅ Edge cases
assert divide(0, 5) == 0
assert divide(-10, 2) == -5
# ๐ฅ Error case
with pytest.raises(ZeroDivisionError):
divide(10, 0)
๐ ๏ธ Best Practices
- ๐ฏ Test One Thing: Each test should verify one specific behavior
- ๐ Clear Test Names:
test_checkout_applies_discount_correctly()
nottest_checkout()
- ๐ก๏ธ Independent Tests: Tests shouldnโt depend on each other
- ๐จ Use Fixtures: Share common setup code between tests
- โจ Run Tests Often: Before commits, after changes, in CI/CD
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Banking System Test Suite
Create regression tests for a banking system:
๐ Requirements:
- โ Account creation with initial balance
- ๐ฐ Deposit and withdrawal operations
- ๐ Overdraft protection
- ๐ Transaction history
- ๐ฏ Interest calculation
- ๐ซ Prevent negative deposits
๐ Bonus Points:
- Add transfer between accounts
- Implement daily withdrawal limits
- Create monthly statement generation
๐ก Solution
๐ Click to see solution
# ๐ฆ banking_system.py
from datetime import datetime
from typing import List, Dict
class BankAccount:
def __init__(self, account_number: str, initial_balance: float = 0):
self.account_number = account_number
self.balance = initial_balance
self.transactions: List[Dict] = []
self.daily_withdrawal_limit = 1000
self.daily_withdrawn = 0
self.last_withdrawal_date = None
# ๐ Record initial deposit if any
if initial_balance > 0:
self._record_transaction("deposit", initial_balance,
"Initial deposit ๐")
def deposit(self, amount: float) -> float:
# ๐ฐ Make a deposit
if amount <= 0:
raise ValueError("Deposit amount must be positive! ๐ซ")
self.balance += amount
self._record_transaction("deposit", amount, "Deposit ๐ต")
return self.balance
def withdraw(self, amount: float) -> float:
# ๐ธ Make a withdrawal with checks
if amount <= 0:
raise ValueError("Withdrawal amount must be positive! ๐ซ")
# Check overdraft
if amount > self.balance:
raise ValueError("Insufficient funds! ๐ฑ")
# Check daily limit
today = datetime.now().date()
if self.last_withdrawal_date != today:
self.daily_withdrawn = 0
self.last_withdrawal_date = today
if self.daily_withdrawn + amount > self.daily_withdrawal_limit:
raise ValueError(f"Daily limit exceeded! Max: ${self.daily_withdrawal_limit} ๐จ")
self.balance -= amount
self.daily_withdrawn += amount
self._record_transaction("withdrawal", amount, "Withdrawal ๐ณ")
return self.balance
def calculate_interest(self, rate: float) -> float:
# ๐ Calculate interest
interest = self.balance * rate
self.deposit(interest)
self._record_transaction("interest", interest, "Interest earned ๐")
return interest
def _record_transaction(self, type: str, amount: float, description: str):
# ๐ Record transaction
self.transactions.append({
"type": type,
"amount": amount,
"description": description,
"timestamp": datetime.now(),
"balance": self.balance
})
# ๐งช test_banking_regression.py
import pytest
from banking_system import BankAccount
from datetime import datetime
class TestBankingRegression:
@pytest.fixture
def account(self):
return BankAccount("123456", 1000)
def test_account_creation(self):
# ๐ฆ Regression: Account creation works
acc = BankAccount("789", 500)
assert acc.account_number == "789"
assert acc.balance == 500
assert len(acc.transactions) == 1 # Initial deposit recorded
def test_deposit_positive_amount(self, account):
# ๐ฐ Regression: Deposits increase balance
new_balance = account.deposit(250)
assert new_balance == 1250
assert account.balance == 1250
def test_deposit_negative_rejected(self, account):
# ๐ซ Regression: Negative deposits blocked
with pytest.raises(ValueError) as exc:
account.deposit(-50)
assert "positive" in str(exc.value)
def test_withdrawal_within_balance(self, account):
# ๐ณ Regression: Valid withdrawals work
new_balance = account.withdraw(300)
assert new_balance == 700
assert account.balance == 700
def test_overdraft_protection(self, account):
# ๐ก๏ธ Regression: Can't withdraw more than balance
with pytest.raises(ValueError) as exc:
account.withdraw(1500)
assert "Insufficient" in str(exc.value)
def test_daily_withdrawal_limit(self, account):
# ๐จ Regression: Daily limit enforced
account.withdraw(600) # First withdrawal OK
account.withdraw(300) # Second OK (total 900)
# Third would exceed limit
with pytest.raises(ValueError) as exc:
account.withdraw(200) # Would be 1100 total
assert "Daily limit" in str(exc.value)
def test_interest_calculation(self, account):
# ๐ Regression: Interest calculated correctly
interest = account.calculate_interest(0.05) # 5% interest
assert interest == 50 # 5% of 1000
assert account.balance == 1050
def test_transaction_history(self, account):
# ๐ Regression: All transactions recorded
account.deposit(100)
account.withdraw(50)
account.calculate_interest(0.02)
# Should have 4 transactions (initial + 3)
assert len(account.transactions) == 4
# Verify transaction types
types = [t["type"] for t in account.transactions]
assert types == ["deposit", "deposit", "withdrawal", "interest"]
def test_zero_withdrawal_rejected(self, account):
# ๐ข Regression: Zero withdrawal not allowed
with pytest.raises(ValueError):
account.withdraw(0)
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create regression tests with confidence ๐ช
- โ Avoid common testing mistakes that trip up beginners ๐ก๏ธ
- โ Apply testing best practices in real projects ๐ฏ
- โ Debug test failures like a pro ๐
- โ Build reliable software with Python! ๐
Remember: Regression testing is your safety net, not your enemy! Itโs here to help you write better, more reliable code. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered regression testing!
Hereโs what to do next:
- ๐ป Practice with the banking system exercise above
- ๐๏ธ Add regression tests to your current project
- ๐ Move on to our next tutorial: Test-Driven Development (TDD)
- ๐ Share your testing journey with others!
Remember: Every bug caught by a test is a bug that didnโt reach your users. Keep testing, keep learning, and most importantly, have fun! ๐
Happy testing! ๐๐โจ