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
- One assertion per test: Each test function should ideally verify only one behavior
- Clear test naming: The test name should convey the purpose of the test
- Use descriptive variable names: Make test code self-documenting
- Avoid complex logic: Tests themselves should be simple and clear
Common Pitfalls
- Floating-point precision: Direct comparison of floats may fail due to precision issues
- Object reference comparison: Use
==to compare content, useisto compare references - Mutable objects: Be aware of modifications affecting lists, dictionaries, and other mutable objects
- Asynchronous operations: Asynchronous code requires special testing methods
Assertion Best Practices Summary
| Scenario | Recommended Approach | Avoid |
|---|---|---|
| Floating-point comparison | math.isclose() | Direct == |
| Exception testing | pytest.raises() | try/except |
| Container checks | in operator | Iterating to search |
| Type checks | isinstance() | type() == |
| Custom error messages | assert expr, msg | Only 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.