The Next Step in Python: Testing with pytest

Yangyang Li

2025-05-05

Python Testing with pytest

Making your code more reliable in 1 hour

Workshop Agenda

  1. Introduction to Testing (10 min)
    • Why test?
    • Types of tests
    • Testing terminology
  2. Getting Started with pytest (15 min)
    • Basic structure
    • Running tests
    • Understanding results
  3. Writing Effective Tests (15 min)
    • Test design patterns
    • Parameterization
  1. Test-Driven Development (15 min)
    • Red-Green-Refactor cycle
    • TDD in practice
    • Benefits & challenges
  2. Q&A and Wrap-up (5 min)

Introduction to Testing

Why Test Your Code?

  • Find bugs early — Before your users do
  • Refactor with confidence — Change code without fear
  • Documentation — Tests show how code should work
  • Design improvement — Testing forces better architecture
  • Professional practice — Industry standard skill

Test Early, Test Often

“Code without tests is broken by design.” — Jacob Kaplan-Moss

Poll Question

How much experience do you have with testing Python code?

A. None - I’m completely new to testing
B. Minimal - I’ve written a few basic tests

C. Moderate - I use pytest occasionally
D. Experienced - I practice TDD regularly

Testing Pyramid

Unit tests
- Test individual functions
- Fast, isolated
- Many tests

Integration tests
- Test component interaction
- Fewer, more complex

Getting Started with pytest

Why pytest?

Advantages
- Simple, Pythonic syntax
- Rich assertion messages
- Powerful fixtures & plugins
- Easy parameterization

vs. unittest

# unittest
self.assertEqual(1 + 1, 2)

# pytest - simpler!
assert 1 + 1 == 2
# Installation
pip install pytest

Basic pytest Structure

# test_example.py
def test_addition():
    # Simple assertions
    assert 1 + 1 == 2


def test_string_methods():
    # pytest shows values on failure
    assert "hello".upper() == "HELLO"
    assert "world".capitalize() == "World"
# Run with:
pytest test_example.py -v

# Output:
# test_example.py::test_addition PASSED
# test_example.py::test_string_methods PASSED

Live Exercise

Let’s write a test together!

  1. Create calculator.py with basic functions:

    def add(a, b): 
        return a + b
  2. Write test_calculator.py:

    from calculator import add
    
    
    def test_add():
        assert add(1, 2) == 3
        assert add(-1, 1) == 0
  3. Run: pytest test_calculator.py -v

Writing Effective Tests

Python Classes Refresher

Classes in Python

  • Blueprint for creating objects
  • Encapsulate data (attributes) and behavior (methods)
  • Create with the class keyword
  • Instantiate with object = ClassName()
  • self refers to the instance
class Calculator:
    def __init__(self, initial_value=0):
        self.result = initial_value
        
    def add(self, value):
        self.result += value
        return self.result

Using a Class

# Create an instance
calc = Calculator(10)

# Call methods
calc.add(5)  # returns 15
calc.add(3)  # returns 18

# Access attributes
print(calc.result)  # 18

The AAA Pattern

Arrange → Act → Assert

The AAA Pattern

# Testing a User class
def test_valid_email():
    # Arrange
    user = User('Test', 'test@example.com')
    
    # Act
    result = user.is_valid_email()
    
    # Assert
    assert result is True
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
        
    def is_valid_email(self):
        return ('@' in self.email 
                and '.' in self.email)

Parameterized Tests

Before

def test_email_valid():
    user = User('Test', 'test@example.com')
    assert user.is_valid_email() is True

def test_email_invalid_no_at():
    user = User('Test', 'invalid-email')
    assert user.is_valid_email() is False
    
def test_email_invalid_no_dot():
    user = User('Test', 'invalid@nodotatall')
    assert user.is_valid_email() is False

After

import pytest

@pytest.mark.parametrize("email,expected", [
    ("test@example.com", True),
    ("invalid-email", False),
    ("another@test.org", True),
    ("missing-dot@com", False),
])
def test_email_validation(email, expected):
    user = User('Test', email)
    assert user.is_valid_email() is expected

Poll Question

What are you most interested in testing?

A. Functions with calculations
B. Data validation logic

C. API interactions
D. Database operations

Test-Driven Development

The TDD Cycle

  1. Red: Write a failing test
  2. Green: Write minimal code to pass
  3. Refactor: Improve without breaking tests

TDD Demo: Shopping Cart

def test_add_item():
    cart = ShoppingCart()
    cart.add_item("apple", 1.0)
    assert cart.total() == 1.0
class ShoppingCart:
    def __init__(self):
        self.items = {}
    
    def add_item(self, name, price):
        self.items[name] = price
        
    def total(self):
        return sum(self.items.values())
def test_apply_discount():
    cart = ShoppingCart()
    cart.add_item("apple", 10.0)
    cart.apply_discount(10)  # 10% discount
    assert cart.total() == 9.0
def apply_discount(self, percentage):
    self._discount = percentage / 100
    
def total(self):
    subtotal = sum(self.items.values())
    return subtotal * (1 - self._discount)

Statistics Function Example

def test_calculate_statistics():
    numbers = [1, 2, 3, 4, 5]
    stats = calculate_statistics(numbers)
    assert stats["min"] == 1
    assert stats["max"] == 5
    assert stats["average"] == 3.0
def calculate_statistics(numbers):
    if not numbers:
        raise ValueError("Cannot calculate statistics of empty list")
    return {
        "min": min(numbers),
        "max": max(numbers),
        "average": sum(numbers) / len(numbers)
    }
def test_empty_list():
    with pytest.raises(ValueError) as excinfo:
        calculate_statistics([])
    assert "empty list" in str(excinfo.value)
    
def test_single_value():
    stats = calculate_statistics([42])
    assert stats["min"] == stats["max"] == stats["average"] == 42

TDD Benefits & Challenges

Benefits
- Forces clear requirements
- Prevents over-engineering
- Built-in regression testing
- Improves API design
- Documentation by example

Challenges
- Learning curve
- Requires discipline
- Can slow initial development
- Test maintenance
- “Test-induced design damage”

Tip

Start small and build your testing habit incrementally!

Poll Question

How might TDD change your workflow?

A. Initial slowdown but long-term gain
B. Clarify requirements before coding

C. Improve my code architecture
D. Still not convinced it’s worth it

Wrap-up

Key Takeaways

  1. Testing is an investment in code quality and maintainability
  2. pytest makes testing approachable with simple syntax and powerful features
  3. Start with simple unit tests and build from there
  4. TDD can guide development and improve your software design
  5. Practice is essential to get comfortable with testing

Resources

Thank You!

Questions?

Contact: yangyang.li@northwestern.edu
GitHub: @cauliyang