Chapter 2: Test Case Writing Fundamentals

Haiyue
11min

Chapter 2: Test Case Writing Fundamentals

Learning Objectives
  • Master the use of assertions (assert)
  • Understand the structure and conventions of test functions
  • Learn how to organize test data
  • Master basic testing patterns

Knowledge Points

Assertion Fundamentals

Assertions are the core of testing. pytest uses Python’s standard assert statement, providing powerful introspection capabilities:

  • Basic assertions: Check if an expression is true
  • Comparison assertions: Compare whether two values are equal, check size relationships, etc.
  • Containment assertions: Check if an element is in a container
  • Type assertions: Check object types
  • Exception assertions: Verify that expected exceptions are raised

Test Function Structure

Follow the AAA Pattern:

  • Arrange: Set up test data and environment
  • Act: Call the functionality being tested
  • Assert: Verify that the result meets expectations

Test Naming Conventions

  • Test function names should clearly describe the test scenario
  • Use the test_ prefix
  • Recommended format: test_<functionality>_<condition>_<expected_result>

Example Code

Basic Assertion Examples

# test_assertions.py

def test_basic_assertions():
    """Basic assertion examples"""
    # Equality assertions
    assert 2 + 2 == 4
    assert "hello" == "hello"

    # Inequality assertions
    assert 3 != 2
    assert "hello" != "world"

    # Boolean assertions
    assert True
    assert not False

    # None assertions
    value = None
    assert value is None

    non_none_value = "test"
    assert non_none_value is not None

def test_comparison_assertions():
    """Comparison assertion examples"""
    # Numeric comparisons
    assert 5 > 3
    assert 2 < 10
    assert 5 >= 5
    assert 3 <= 3

    # String comparisons
    assert "apple" < "banana"  # Lexicographic order
    assert len("hello") == 5

def test_container_assertions():
    """Container assertion examples"""
    my_list = [1, 2, 3, 4, 5]
    my_dict = {"name": "Alice", "age": 30}
    my_set = {1, 2, 3}

    # Membership checks
    assert 3 in my_list
    assert 6 not in my_list
    assert "name" in my_dict
    assert "address" not in my_dict

    # Length checks
    assert len(my_list) == 5
    assert len(my_dict) == 2

    # Empty container checks
    empty_list = []
    assert not empty_list  # Empty list is False
    assert len(empty_list) == 0

String and Type Assertions

# test_string_types.py

def test_string_assertions():
    """String assertion examples"""
    text = "Hello, World!"

    # String containment
    assert "Hello" in text
    assert "goodbye" not in text

    # String methods
    assert text.startswith("Hello")
    assert text.endswith("!")
    assert text.isupper() == False
    assert text.lower() == "hello, world!"

    # Empty strings
    empty_str = ""
    assert not empty_str
    assert len(empty_str) == 0

def test_type_assertions():
    """Type assertion examples"""
    # Basic type checks
    assert isinstance(42, int)
    assert isinstance(3.14, float)
    assert isinstance("hello", str)
    assert isinstance([1, 2, 3], list)
    assert isinstance({"key": "value"}, dict)

    # Inheritance relationship checks
    class Animal:
        pass

    class Dog(Animal):
        pass

    my_dog = Dog()
    assert isinstance(my_dog, Dog)
    assert isinstance(my_dog, Animal)  # Inheritance relationship

    # Type mismatch
    assert not isinstance("123", int)

Numeric and Precision Assertions

# test_numeric.py
import math

def test_numeric_assertions():
    """Numeric assertion examples"""
    # Integer operations
    assert 2 + 3 == 5
    assert 10 - 4 == 6
    assert 3 * 4 == 12
    assert 15 // 4 == 3  # Integer division
    assert 15 % 4 == 3   # Modulo

    # Floating-point comparisons (watch for precision issues)
    result = 0.1 + 0.2
    # Direct comparison might fail: assert result == 0.3
    # Use precision comparison
    assert abs(result - 0.3) < 1e-10

    # Better approach: use math.isclose()
    assert math.isclose(result, 0.3, rel_tol=1e-9)

    # Infinity and NaN
    assert math.isinf(float('inf'))
    assert math.isnan(float('nan'))

def test_range_assertions():
    """Range assertion examples"""
    value = 15

    # Range checks
    assert 10 <= value <= 20
    assert value in range(10, 21)

    # List ranges
    scores = [85, 92, 78, 96, 88]
    assert all(score >= 0 for score in scores)
    assert all(score <= 100 for score in scores)
    assert any(score > 90 for score in scores)

Complex Data Structure Assertions

# test_complex_data.py

def test_list_assertions():
    """List assertion examples"""
    numbers = [1, 2, 3, 4, 5]

    # List content
    assert numbers[0] == 1
    assert numbers[-1] == 5
    assert numbers[1:3] == [2, 3]

    # List operations
    assert sorted(numbers) == numbers  # Already sorted
    assert sum(numbers) == 15
    assert max(numbers) == 5
    assert min(numbers) == 1

    # List comparison
    other_numbers = [1, 2, 3, 4, 5]
    assert numbers == other_numbers
    assert numbers is not other_numbers  # Not the same object

def test_dict_assertions():
    """Dictionary assertion examples"""
    user = {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

    # Key-value checks
    assert user["name"] == "Alice"
    assert user.get("phone") is None
    assert user.get("phone", "N/A") == "N/A"

    # Key and value existence
    assert "name" in user
    assert "Alice" in user.values()
    assert ("id", 1) in user.items()

    # Dictionary structure
    expected_keys = {"id", "name", "email", "age"}
    assert set(user.keys()) == expected_keys

def test_nested_data_assertions():
    """Nested data structure assertions"""
    data = {
        "users": [
            {"id": 1, "name": "Alice", "active": True},
            {"id": 2, "name": "Bob", "active": False}
        ],
        "total": 2
    }

    # Nested access
    assert data["total"] == 2
    assert len(data["users"]) == 2
    assert data["users"][0]["name"] == "Alice"
    assert data["users"][1]["active"] == False

    # Complex conditions
    active_users = [user for user in data["users"] if user["active"]]
    assert len(active_users) == 1
    assert active_users[0]["name"] == "Alice"

Custom Assertion Functions

# test_custom_assertions.py

def is_even(number):
    """Check if a number is even"""
    return number % 2 == 0

def is_valid_email(email):
    """Simple email validation"""
    return "@" in email and "." in email

def test_custom_assertions():
    """Custom assertion function examples"""
    # Using custom assertion functions
    assert is_even(4)
    assert not is_even(5)

    assert is_valid_email("user@example.com")
    assert not is_valid_email("invalid-email")

# More complex custom assertions
def assert_user_valid(user):
    """Validate that a user object is valid"""
    assert isinstance(user, dict), "User must be a dictionary"
    assert "id" in user, "User must have an ID"
    assert "name" in user, "User must have a name"
    assert isinstance(user["id"], int), "User ID must be an integer"
    assert isinstance(user["name"], str), "User name must be a string"
    assert len(user["name"]) > 0, "User name cannot be empty"

def test_user_validation():
    """User validation test"""
    valid_user = {"id": 1, "name": "Alice"}
    assert_user_valid(valid_user)

    # This test will pass because user data is valid

Error Messages and Debugging

# test_debugging.py

def test_assertion_messages():
    """Assertions with custom error messages"""
    user_age = 17

    # With custom error message
    assert user_age >= 18, f"User age {user_age} does not meet minimum requirement of 18"

    # This test will fail and display the custom message

def test_debugging_info():
    """Debugging information examples"""
    numbers = [1, 2, 3, 4, 5]
    target = 6

    # pytest will automatically display variable values
    assert target in numbers  # On failure, will display the contents of numbers

    # Debugging complex expressions
    result = sum(numbers)
    expected = 20  # Wrong expected value
    assert result == expected  # Will display result=15, expected=20

Test Organization Best Practices

Test Class Organization

# test_calculator_class.py

class TestCalculator:
    """Calculator test class"""

    def setup_method(self):
        """Execute before each test method"""
        self.calc = Calculator()

    def test_addition(self):
        """Test 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"""
        assert self.calc.subtract(5, 3) == 2
        assert self.calc.subtract(0, 5) == -5

    def test_multiplication(self):
        """Test multiplication"""
        assert self.calc.multiply(3, 4) == 12
        assert self.calc.multiply(-2, 3) == -6
        assert self.calc.multiply(0, 5) == 0

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b
Test Writing Tips
  1. One assertion per test: Each test function should ideally verify only one behavior
  2. Clear test naming: The test name should convey the purpose of the test
  3. Use descriptive variable names: Make test code self-documenting
  4. Avoid complex logic: Tests themselves should be simple and clear
Common Pitfalls
  1. Floating-point precision: Direct comparison of floats may fail due to precision issues
  2. Object reference comparison: Use == to compare content, use is to compare references
  3. Mutable objects: Be aware of modifications affecting lists, dictionaries, and other mutable objects
  4. Asynchronous operations: Asynchronous code requires special testing methods

Assertion Best Practices Summary

ScenarioRecommended ApproachAvoid
Floating-point comparisonmath.isclose()Direct ==
Exception testingpytest.raises()try/except
Container checksin operatorIterating to search
Type checksisinstance()type() ==
Custom error messagesassert expr, msgOnly assert expr

With this, we’ve mastered the various uses of assertions in pytest and the basic skills for writing test cases, laying a solid foundation for learning advanced features.