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 pytest parametrization! ๐ Have you ever found yourself writing the same test over and over again with different inputs? Itโs like being a chef who has to cook the same dish 100 times just to test different spice levels! ๐ณ
Today, weโll discover how pytest parametrization can transform your testing experience. Whether youโre testing calculators ๐งฎ, validating user inputs ๐, or checking edge cases ๐, parametrization makes your tests cleaner, more maintainable, and way more powerful!
By the end of this tutorial, youโll be writing tests that cover dozens of scenarios with minimal code. Letโs dive in! ๐โโ๏ธ
๐ Understanding Pytest Parametrization
๐ค What is Parametrization?
Parametrization is like having a magic test multiplier! ๐ฉโจ Think of it as a factory that takes one test function and creates multiple test cases from it automatically.
Instead of writing:
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 5) == 5
You write ONE test that handles ALL cases! ๐
๐ก Why Use Parametrization?
Hereโs why developers love parametrization:
- DRY Principle ๐ต: Donโt Repeat Yourself - write once, test many
- Better Coverage ๐: Easy to add new test cases
- Cleaner Code โจ: Less duplication, more readability
- Time Saver โฐ: Add test cases in seconds
- Edge Case Hunter ๐ฏ: Catch those sneaky bugs
Real-world example: Imagine testing a password validator ๐. With parametrization, you can test weak passwords, strong passwords, special characters, and length requirements all in one elegant test!
๐ง Basic Syntax and Usage
๐ Simple Parametrization
Letโs start with a friendly example:
import pytest
# ๐ Hello, parametrization!
@pytest.mark.parametrize("input,expected", [
(2, 4), # ๐ข Test case 1
(3, 9), # ๐ข Test case 2
(4, 16), # ๐ข Test case 3
(5, 25), # ๐ข Test case 4
])
def test_square(input, expected):
"""โจ One test, multiple cases!"""
assert input ** 2 == expected
๐ก Explanation: The @pytest.mark.parametrize
decorator takes two arguments:
- Parameter names as a string (comma-separated)
- A list of tuples with test data
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Multiple parameters
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5), # โ Addition
(10, -5, 5), # โ Subtraction-like
(0, 0, 0), # ๐ฏ Edge case
])
def test_addition(a, b, expected):
assert a + b == expected
# ๐จ Pattern 2: Single parameter with descriptive IDs
@pytest.mark.parametrize("password", [
"short", # ๐ซ Too short
"nouppercase123", # ๐ซ No uppercase
"NOLOWERCASE123", # ๐ซ No lowercase
"NoNumbers!", # ๐ซ No numbers
"ValidPass123!", # โ
Perfect!
], ids=["too_short", "no_upper", "no_lower", "no_digits", "valid"])
def test_password_strength(password):
# Your password validation logic here
pass
# ๐ Pattern 3: Using pytest.param for better organization
@pytest.mark.parametrize("test_input,expected", [
pytest.param(0, 0, id="zero"),
pytest.param(1, 1, id="one"),
pytest.param(-1, 1, id="negative"),
pytest.param(100, 10000, marks=pytest.mark.slow), # ๐ Mark slow tests
])
def test_square_advanced(test_input, expected):
assert test_input ** 2 == expected
๐ก Practical Examples
๐ Example 1: E-commerce Price Calculator
Letโs build something real - a shopping cart discount calculator:
import pytest
from decimal import Decimal
# ๐๏ธ Our discount calculator
class DiscountCalculator:
def calculate_final_price(self, price, discount_percent, is_vip=False):
"""Calculate price after discount ๐ฐ"""
discount = Decimal(str(price)) * Decimal(str(discount_percent)) / 100
final_price = Decimal(str(price)) - discount
# ๐ VIP members get extra 5% off!
if is_vip:
final_price *= Decimal('0.95')
return float(final_price)
# ๐งช Test with multiple scenarios
@pytest.mark.parametrize("price,discount,is_vip,expected", [
# Regular customers
(100, 0, False, 100.00), # ๐ท๏ธ No discount
(100, 10, False, 90.00), # ๐ท๏ธ 10% off
(100, 50, False, 50.00), # ๐ท๏ธ Half price sale!
# VIP customers get extra perks! ๐
(100, 0, True, 95.00), # ๐ VIP base discount
(100, 10, True, 85.50), # ๐ VIP + 10% off
(100, 50, True, 47.50), # ๐ VIP + half price!
# Edge cases ๐ฏ
(0, 10, False, 0), # ๐ Free item
(49.99, 20, False, 39.99), # ๐ต Decimal prices
])
def test_discount_calculator(price, discount, is_vip, expected):
calc = DiscountCalculator()
result = calc.calculate_final_price(price, discount, is_vip)
assert abs(result - expected) < 0.01 # ๐ฐ Handle floating point precision
# ๐ Run with: pytest -v to see all test cases!
๐ฏ Try it yourself: Add test cases for invalid inputs like negative prices or discounts over 100%!
๐ฎ Example 2: Game Character Stats Validator
Letโs make testing fun with a game character system:
import pytest
# ๐ฎ Game character stats
class GameCharacter:
def __init__(self, name, level, health, attack, defense):
self.name = name
self.level = level
self.health = health
self.attack = attack
self.defense = defense
def calculate_power_level(self):
"""Calculate overall power level ๐ช"""
return (self.health * 0.5) + (self.attack * 2) + (self.defense * 1.5)
def can_defeat(self, enemy_power):
"""Check if character can win โ๏ธ"""
return self.calculate_power_level() > enemy_power
# ๐งช Test different character builds
@pytest.mark.parametrize("name,level,health,attack,defense,expected_power", [
# Different character classes ๐ญ
("Warrior", 10, 100, 15, 20, 110.0), # ๐ก๏ธ Tank build
("Mage", 10, 60, 25, 10, 95.0), # ๐งโโ๏ธ Glass cannon
("Rogue", 10, 80, 20, 15, 82.5), # ๐ก๏ธ Balanced
("Paladin", 10, 90, 18, 18, 108.0), # โ๏ธ All-rounder
# Level progression ๐
("Noob", 1, 30, 5, 5, 27.5), # ๐ถ Beginner
("Pro", 50, 500, 100, 80, 570.0), # ๐ End game
("Legend", 99, 999, 200, 150, 924.5), # ๐ Max level!
])
def test_character_power_calculation(name, level, health, attack, defense, expected_power):
character = GameCharacter(name, level, health, attack, defense)
assert character.calculate_power_level() == expected_power
print(f" {name} has {expected_power} power! {'๐ช' * (level // 20)}")
# ๐ฏ Test combat scenarios
@pytest.mark.parametrize("attacker_stats,enemy_power,should_win", [
((100, 20, 15), 80, True), # ๐ช Strong vs weak
((60, 10, 10), 100, False), # ๐ฐ Weak vs strong
((80, 15, 15), 60, True), # โ๏ธ Fair fight
], ids=["strong_wins", "weak_loses", "balanced_fight"])
def test_combat_outcomes(attacker_stats, enemy_power, should_win):
health, attack, defense = attacker_stats
hero = GameCharacter("Hero", 10, health, attack, defense)
assert hero.can_defeat(enemy_power) == should_win
๐ Advanced Concepts
๐งโโ๏ธ Multiple Parameter Sets
When youโre ready to level up, combine multiple parametrize decorators:
# ๐ฏ Test all combinations!
@pytest.mark.parametrize("browser", ["chrome", "firefox", "safari"])
@pytest.mark.parametrize("resolution", ["mobile", "tablet", "desktop"])
@pytest.mark.parametrize("theme", ["light", "dark"])
def test_ui_compatibility(browser, resolution, theme):
"""Test UI in all possible combinations ๐"""
print(f"Testing {theme} theme on {browser} at {resolution} resolution")
# This creates 3 ร 3 ร 2 = 18 test cases! ๐
# ๐ช Using indirect parametrization for fixtures
@pytest.mark.parametrize("db_type", ["sqlite", "postgres", "mysql"], indirect=True)
def test_database_operations(db_type):
"""Test with different database backends ๐๏ธ"""
# db_type is actually a fixture that sets up the database!
pass
๐๏ธ Dynamic Parametrization
For the brave developers - generate test cases dynamically:
# ๐ Generate test cases programmatically
def generate_fibonacci_tests():
"""Generate Fibonacci test cases ๐ข"""
fib_cases = [
(0, 0),
(1, 1),
(2, 1),
(3, 2),
(4, 3),
(5, 5),
(6, 8),
(10, 55),
]
return [(n, expected) for n, expected in fib_cases]
@pytest.mark.parametrize("n,expected", generate_fibonacci_tests())
def test_fibonacci(n, expected):
"""Test Fibonacci sequence ๐"""
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
assert fib(n) == expected
# ๐ซ Parametrize with custom objects
class TestCase:
def __init__(self, input_data, expected, description):
self.input_data = input_data
self.expected = expected
self.description = description
def __repr__(self):
return f"<{self.description}>"
test_scenarios = [
TestCase([1, 2, 3], 6, "positive_numbers"),
TestCase([-1, -2, -3], -6, "negative_numbers"),
TestCase([0, 0, 0], 0, "all_zeros"),
TestCase([1, -1, 2, -2], 0, "mixed_numbers"),
]
@pytest.mark.parametrize("test_case", test_scenarios)
def test_sum_with_objects(test_case):
"""Test sum with custom test objects ๐"""
result = sum(test_case.input_data)
assert result == test_case.expected
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting String Quotes in Parameter Names
# โ Wrong - This will cause an error!
@pytest.mark.parametrize(input, expected, [(1, 2), (2, 4)])
def test_double(input, expected):
assert input * 2 == expected
# โ
Correct - Parameter names must be strings!
@pytest.mark.parametrize("input,expected", [(1, 2), (2, 4)])
def test_double(input, expected):
assert input * 2 == expected
๐คฏ Pitfall 2: Mutable Default Arguments
# โ Dangerous - Shared mutable state!
@pytest.mark.parametrize("items,default", [
([1, 2, 3], []), # ๐ฅ Empty list is mutable!
])
def test_append_items(items, default):
for item in items:
default.append(item) # Modifies the shared list!
return default
# โ
Safe - Create new instances!
@pytest.mark.parametrize("items", [
[1, 2, 3],
[4, 5, 6],
])
def test_append_items_safe(items):
result = [] # โจ Fresh list each time
for item in items:
result.append(item)
assert len(result) == len(items)
๐ญ Pitfall 3: Too Many Test Cases
# โ Overwhelming - 1000 test cases!
@pytest.mark.parametrize("x", range(1000))
def test_something(x):
assert x >= 0
# โ
Strategic - Test boundaries and samples!
@pytest.mark.parametrize("x", [
0, # ๐ฏ Lower boundary
1, # ๐ฏ Just above lower
50, # ๐ฏ Middle value
99, # ๐ฏ Just below upper
100, # ๐ฏ Upper boundary
])
def test_something_smart(x):
assert 0 <= x <= 100
๐ ๏ธ Best Practices
-
๐ฏ Use Descriptive IDs: Help identify failing tests
@pytest.mark.parametrize("input,expected", [ (2, 4), (3, 9), ], ids=["two_squared", "three_squared"])
-
๐ Group Related Tests: Keep similar test cases together
# Group by test category POSITIVE_CASES = [(1, 1), (2, 4), (3, 9)] NEGATIVE_CASES = [(-1, 1), (-2, 4), (-3, 9)] EDGE_CASES = [(0, 0), (1000000, 1000000000000)]
-
๐ก๏ธ Test Edge Cases: Always include boundaries
-
๐จ Keep It Readable: Donโt sacrifice clarity for cleverness
-
โจ Use pytest.param: For better organization and marking
๐งช Hands-On Exercise
๐ฏ Challenge: Build a URL Validator Test Suite
Create a comprehensive test suite for a URL validator:
๐ Requirements:
- โ Test valid URLs (http, https, ftp)
- ๐ซ Test invalid URLs (missing protocol, invalid characters)
- ๐ Test different domains (.com, .org, .io)
- ๐ Test URL length limits
- ๐จ Each test needs clear identification!
๐ Bonus Points:
- Add tests for query parameters
- Test international domains
- Include port number validation
๐ก Solution
๐ Click to see solution
import pytest
import re
# ๐ Our URL validator
class URLValidator:
def __init__(self):
# Simple regex for demonstration ๐ฏ
self.url_pattern = re.compile(
r'^(https?|ftp)://' # Protocol
r'([a-zA-Z0-9.-]+)' # Domain
r'(\.[a-zA-Z]{2,})' # TLD
r'(:[0-9]+)?' # Optional port
r'(/.*)?$' # Optional path
)
def is_valid(self, url):
"""Check if URL is valid ๐"""
if not url or len(url) > 2048: # URL length limit
return False
return bool(self.url_pattern.match(url))
# ๐งช Comprehensive test suite
@pytest.mark.parametrize("url,expected,test_id", [
# Valid URLs โ
("http://example.com", True, "basic_http"),
("https://example.com", True, "basic_https"),
("ftp://files.example.com", True, "ftp_protocol"),
("https://sub.domain.example.com", True, "subdomain"),
("https://example.com:8080", True, "with_port"),
("https://example.com/path/to/page", True, "with_path"),
("https://example.com/search?q=python", True, "with_query"),
# Different TLDs ๐
("https://example.org", True, "dot_org"),
("https://example.io", True, "dot_io"),
("https://example.co.uk", True, "country_tld"),
# Invalid URLs โ
("example.com", False, "missing_protocol"),
("http://", False, "missing_domain"),
("http://example", False, "missing_tld"),
("ht!tp://example.com", False, "invalid_protocol"),
("http://exam ple.com", False, "space_in_domain"),
("", False, "empty_string"),
("http://.com", False, "missing_domain_name"),
# Edge cases ๐ฏ
("http://localhost", False, "localhost_no_tld"),
("https://192.168.1.1", False, "ip_address"),
("http://a.b", True, "minimal_valid"),
("https://" + "a" * 2040 + ".com", False, "too_long"),
])
def test_url_validation(url, expected, test_id):
"""Test URL validation with multiple cases ๐"""
validator = URLValidator()
result = validator.is_valid(url)
assert result == expected, f"Failed for {test_id}: {url}"
# ๐ Print test summary
status = "โ
PASS" if result == expected else "โ FAIL"
print(f"{status} [{test_id}]: {url[:50]}...")
# ๐ฏ Separate test for extremely long URLs
@pytest.mark.parametrize("length", [100, 500, 1000, 2048, 2049])
def test_url_length_limits(length):
"""Test URL length boundaries ๐"""
validator = URLValidator()
base_url = "https://example.com/"
padding = "a" * (length - len(base_url))
test_url = base_url + padding
expected = length <= 2048
assert validator.is_valid(test_url) == expected
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Write parametrized tests that cover multiple scenarios ๐ฏ
- โ Avoid code duplication in your test suites ๐ต
- โ Use advanced features like indirect params and marks ๐
- โ Test edge cases efficiently without repetition ๐ก๏ธ
- โ Debug failed tests with clear test IDs ๐
Remember: Good tests are the foundation of reliable software. Parametrization makes writing comprehensive tests a breeze! ๐ฌ๏ธ
๐ค Next Steps
Congratulations! ๐ Youโve mastered pytest parametrization!
Hereโs what to do next:
- ๐ป Practice with the URL validator exercise
- ๐๏ธ Refactor your existing tests to use parametrization
- ๐ Explore pytest fixtures for even more power
- ๐ Share your parametrized tests with your team!
Remember: Every test you parametrize saves time and catches more bugs. Keep testing, keep learning, and most importantly, have fun! ๐
Happy testing! ๐๐งชโจ