+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 203 of 343

๐Ÿ“˜ Pytest Parametrization: Multiple Test Cases

Master pytest parametrization: multiple test cases in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. DRY Principle ๐ŸŒต: Donโ€™t Repeat Yourself - write once, test many
  2. Better Coverage ๐Ÿ“Š: Easy to add new test cases
  3. Cleaner Code โœจ: Less duplication, more readability
  4. Time Saver โฐ: Add test cases in seconds
  5. 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

  1. ๐ŸŽฏ Use Descriptive IDs: Help identify failing tests

    @pytest.mark.parametrize("input,expected", [
        (2, 4),
        (3, 9),
    ], ids=["two_squared", "three_squared"])
  2. ๐Ÿ“ 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)]
  3. ๐Ÿ›ก๏ธ Test Edge Cases: Always include boundaries

  4. ๐ŸŽจ Keep It Readable: Donโ€™t sacrifice clarity for cleverness

  5. โœจ 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:

  1. ๐Ÿ’ป Practice with the URL validator exercise
  2. ๐Ÿ—๏ธ Refactor your existing tests to use parametrization
  3. ๐Ÿ“š Explore pytest fixtures for even more power
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿงชโœจ