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! 🎉 Have you ever wondered how professional developers ensure their code works correctly? The answer is testing, and pytest is Python’s most powerful and fun testing framework!
You’ll discover how pytest can transform your Python development experience. Whether you’re building web applications 🌐, data science projects 📊, or command-line tools 🛠️, understanding pytest is essential for writing robust, maintainable code.
By the end of this tutorial, you’ll feel confident writing tests that catch bugs before they reach production! Let’s dive in! 🏊♂️
📚 Understanding Pytest
🤔 What is Pytest?
Pytest is like having a safety net while performing acrobatics 🎪. Think of it as your code’s personal quality inspector that automatically checks if everything works as expected before you ship it to users!
In Python terms, pytest is a testing framework that makes writing tests simple, readable, and even enjoyable! This means you can:
- ✨ Write tests with minimal boilerplate code
- 🚀 Run tests blazingly fast with smart test discovery
- 🛡️ Catch bugs before they reach production
- 📊 Get detailed reports about what’s working and what’s not
💡 Why Use Pytest?
Here’s why developers love pytest:
- Simple Syntax 🔒: Just use plain
assert
statements - Powerful Features 💻: Fixtures, parametrization, and plugins
- Automatic Discovery 📖: Finds tests without configuration
- Detailed Output 🔧: Clear error messages that help debugging
Real-world example: Imagine building an e-commerce site 🛒. With pytest, you can automatically test that prices calculate correctly, inventory updates properly, and users can successfully complete purchases!
🔧 Basic Syntax and Usage
📝 Simple Example
Let’s start with a friendly example:
# 👋 Hello, pytest!
def add_numbers(a, b):
"""Add two numbers together"""
return a + b
# 🧪 Our first test
def test_add_numbers():
# 🎯 Simple assertion
assert add_numbers(2, 3) == 5
assert add_numbers(-1, 1) == 0
assert add_numbers(0, 0) == 0
print("✅ All tests passed!")
# 🎨 Testing with more complex data
def calculate_discount(price, discount_percent):
"""Calculate discounted price"""
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100")
return price * (1 - discount_percent / 100)
def test_calculate_discount():
# 💰 Test normal cases
assert calculate_discount(100, 20) == 80
assert calculate_discount(50, 50) == 25
# 🎯 Test edge cases
assert calculate_discount(100, 0) == 100
assert calculate_discount(100, 100) == 0
💡 Explanation: Notice how we use simple assert
statements! Pytest automatically discovers functions starting with test_
and runs them.
🎯 Common Patterns
Here are patterns you’ll use daily:
import pytest
# 🏗️ Pattern 1: Testing exceptions
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero! 🚫")
return a / b
def test_divide_by_zero():
# 🛡️ Test that exception is raised
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
# 🎨 Pattern 2: Using fixtures
@pytest.fixture
def shopping_cart():
"""Create a test shopping cart 🛒"""
return {
"items": [],
"total": 0
}
def test_empty_cart(shopping_cart):
# 📦 Test with fixture
assert len(shopping_cart["items"]) == 0
assert shopping_cart["total"] == 0
# 🔄 Pattern 3: Parametrized tests
@pytest.mark.parametrize("input_value,expected", [
(5, 25), # 5² = 25
(0, 0), # 0² = 0
(-3, 9), # (-3)² = 9
(10, 100), # 10² = 100
])
def test_square_function(input_value, expected):
# 🎯 Test multiple cases with one function
assert input_value ** 2 == expected
💡 Practical Examples
🛒 Example 1: Shopping Cart Testing
Let’s build something real:
# 🛍️ Our shopping cart class
class ShoppingCart:
def __init__(self):
self.items = []
self.discount_code = None
def add_item(self, name, price, quantity=1):
"""Add item to cart 🛒"""
item = {
"name": name,
"price": price,
"quantity": quantity,
"emoji": self._get_emoji(name)
}
self.items.append(item)
print(f"Added {item['emoji']} {name} to cart!")
def _get_emoji(self, name):
"""Get emoji for item 🎨"""
emojis = {
"book": "📚",
"coffee": "☕",
"laptop": "💻",
"pizza": "🍕"
}
return emojis.get(name.lower(), "📦")
def get_total(self):
"""Calculate total price 💰"""
subtotal = sum(item["price"] * item["quantity"]
for item in self.items)
if self.discount_code == "SAVE20":
return subtotal * 0.8
return subtotal
def apply_discount(self, code):
"""Apply discount code 🎟️"""
valid_codes = ["SAVE20", "WELCOME10"]
if code in valid_codes:
self.discount_code = code
return True
return False
# 🧪 Test our shopping cart
import pytest
@pytest.fixture
def cart():
"""Create a fresh cart for each test 🛒"""
return ShoppingCart()
def test_add_items_to_cart(cart):
# ➕ Test adding items
cart.add_item("Coffee", 4.99)
cart.add_item("Book", 19.99, quantity=2)
assert len(cart.items) == 2
assert cart.items[0]["name"] == "Coffee"
assert cart.items[1]["quantity"] == 2
def test_calculate_total(cart):
# 💰 Test price calculation
cart.add_item("Laptop", 999.99)
cart.add_item("Coffee", 4.99, quantity=3)
expected_total = 999.99 + (4.99 * 3)
assert cart.get_total() == pytest.approx(expected_total)
def test_discount_codes(cart):
# 🎟️ Test discount functionality
cart.add_item("Pizza", 15.00)
cart.add_item("Coffee", 5.00)
# Test invalid code
assert cart.apply_discount("INVALID") == False
assert cart.get_total() == 20.00
# Test valid code
assert cart.apply_discount("SAVE20") == True
assert cart.get_total() == 16.00 # 20% off
🎯 Try it yourself: Add a test for removing items from the cart!
🎮 Example 2: Game Score Testing
Let’s make it fun:
# 🏆 Game scoring system
class GameScoreTracker:
def __init__(self, player_name):
self.player_name = player_name
self.score = 0
self.level = 1
self.achievements = []
self.combo_multiplier = 1
def add_points(self, points):
"""Add points with combo multiplier 🎯"""
actual_points = points * self.combo_multiplier
self.score += actual_points
# 🎊 Level up every 1000 points
new_level = (self.score // 1000) + 1
if new_level > self.level:
self.level_up(new_level)
return actual_points
def level_up(self, new_level):
"""Level up the player 📈"""
self.level = new_level
achievement = f"🏆 Reached Level {new_level}!"
self.achievements.append(achievement)
print(f"🎉 {self.player_name} {achievement}")
def activate_combo(self, multiplier):
"""Activate score combo 🔥"""
if 1 <= multiplier <= 5:
self.combo_multiplier = multiplier
return True
return False
def get_rank(self):
"""Get player rank based on score 🥇"""
if self.score >= 10000:
return "🥇 Master"
elif self.score >= 5000:
return "🥈 Expert"
elif self.score >= 1000:
return "🥉 Advanced"
else:
return "🌟 Beginner"
# 🧪 Test the game system
@pytest.fixture
def game():
"""Create a new game session 🎮"""
return GameScoreTracker("TestPlayer")
def test_basic_scoring(game):
# 🎯 Test normal point addition
points_added = game.add_points(100)
assert points_added == 100
assert game.score == 100
assert game.level == 1
def test_level_progression(game):
# 📈 Test leveling up
game.add_points(500)
assert game.level == 1
game.add_points(600) # Total: 1100
assert game.level == 2
assert "🏆 Reached Level 2!" in game.achievements
def test_combo_system(game):
# 🔥 Test combo multipliers
assert game.activate_combo(3) == True
points = game.add_points(100)
assert points == 300 # 100 * 3
# Test invalid combo
assert game.activate_combo(10) == False
@pytest.mark.parametrize("score,expected_rank", [
(0, "🌟 Beginner"),
(999, "🌟 Beginner"),
(1000, "🥉 Advanced"),
(5000, "🥈 Expert"),
(10000, "🥇 Master"),
])
def test_ranking_system(score, expected_rank):
# 🥇 Test all rank levels
game = GameScoreTracker("RankTest")
game.score = score
assert game.get_rank() == expected_rank
🚀 Advanced Concepts
🧙♂️ Advanced Topic 1: Custom Fixtures
When you’re ready to level up, try this advanced pattern:
# 🎯 Advanced fixture with cleanup
import tempfile
import os
@pytest.fixture
def temp_database():
"""Create temporary test database 💾"""
# Setup: Create temp file
db_file = tempfile.NamedTemporaryFile(delete=False)
db_path = db_file.name
# 🎨 Yield the resource
yield db_path
# Cleanup: Remove temp file
os.unlink(db_path)
print("🧹 Cleaned up test database")
def test_with_temp_database(temp_database):
# 💡 Use the temporary database
assert os.path.exists(temp_database)
# Your database tests here...
# 🪄 Fixture with parameters
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_connection(request):
"""Test with multiple databases 🗄️"""
db_type = request.param
if db_type == "sqlite":
return {"type": "sqlite", "emoji": "🪶"}
elif db_type == "postgres":
return {"type": "postgres", "emoji": "🐘"}
else:
return {"type": "mysql", "emoji": "🐬"}
def test_database_types(database_connection):
# 🚀 Automatically runs for each database
assert "type" in database_connection
assert "emoji" in database_connection
🏗️ Advanced Topic 2: Mocking and Patching
For the brave developers:
# 🚀 Advanced mocking techniques
from unittest.mock import Mock, patch
import requests
class WeatherService:
def get_weather(self, city):
"""Get weather from API 🌤️"""
response = requests.get(f"http://api.weather.com/{city}")
return response.json()
def get_temperature(self, city):
"""Get temperature for city 🌡️"""
weather = self.get_weather(city)
return weather["temperature"]
# 🧪 Test with mocking
def test_weather_service():
service = WeatherService()
# 🎭 Mock the get_weather method
service.get_weather = Mock(return_value={
"temperature": 22,
"condition": "☀️ Sunny"
})
temp = service.get_temperature("TestCity")
assert temp == 22
service.get_weather.assert_called_once_with("TestCity")
# 🎨 Using patch decorator
@patch('requests.get')
def test_weather_api_call(mock_get):
# 🔧 Configure mock response
mock_response = Mock()
mock_response.json.return_value = {
"temperature": 25,
"condition": "⛅ Partly Cloudy"
}
mock_get.return_value = mock_response
service = WeatherService()
result = service.get_weather("London")
assert result["temperature"] == 25
mock_get.assert_called_with("http://api.weather.com/London")
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Testing Implementation Instead of Behavior
# ❌ Wrong way - testing internal details
class Calculator:
def __init__(self):
self._internal_state = 0 # Private attribute
def add(self, value):
self._internal_state += value
return self._internal_state
def test_calculator_bad():
calc = Calculator()
calc.add(5)
# ❌ Don't test private attributes!
assert calc._internal_state == 5
# ✅ Correct way - test the behavior
def test_calculator_good():
calc = Calculator()
result = calc.add(5)
assert result == 5 # ✅ Test the public interface!
🤯 Pitfall 2: Forgetting to Test Edge Cases
# ❌ Incomplete testing
def divide_numbers(a, b):
return a / b
def test_divide_incomplete():
assert divide_numbers(10, 2) == 5
# 💥 What about division by zero?
# ✅ Complete testing with edge cases
def test_divide_complete():
# Normal cases
assert divide_numbers(10, 2) == 5
assert divide_numbers(-10, 2) == -5
# 🛡️ Edge cases
with pytest.raises(ZeroDivisionError):
divide_numbers(10, 0)
# 🎯 Float precision
assert divide_numbers(1, 3) == pytest.approx(0.333, rel=1e-3)
🛠️ Best Practices
- 🎯 Test Behavior, Not Implementation: Focus on what the code does, not how
- 📝 Use Descriptive Test Names:
test_user_can_login_with_valid_credentials
- 🛡️ One Assertion Per Test Concept: Keep tests focused and clear
- 🎨 Use Fixtures for Setup: Don’t repeat test setup code
- ✨ Keep Tests Fast: Mock external dependencies
🧪 Hands-On Exercise
🎯 Challenge: Build a Library Management System
Create a test-driven library system:
📋 Requirements:
- ✅ Add and remove books with ISBN tracking
- 🏷️ Check out and return books
- 👤 Track member borrowing limits
- 📅 Calculate late fees
- 🎨 Each book needs a genre emoji!
🚀 Bonus Points:
- Add reservation system
- Implement book recommendations
- Create borrowing history reports
💡 Solution
🔍 Click to see solution
# 🎯 Library management system with tests!
import pytest
from datetime import datetime, timedelta
class Book:
def __init__(self, isbn, title, author, genre):
self.isbn = isbn
self.title = title
self.author = author
self.genre = genre
self.available = True
self.borrowed_by = None
self.due_date = None
self.emoji = self._get_genre_emoji()
def _get_genre_emoji(self):
"""Get emoji for book genre 📚"""
emojis = {
"fiction": "🧙",
"science": "🔬",
"history": "📜",
"cooking": "🍳",
"technology": "💻"
}
return emojis.get(self.genre.lower(), "📖")
class Library:
def __init__(self):
self.books = {}
self.members = {}
self.borrow_limit = 3
self.daily_fine = 0.50
def add_book(self, book):
"""Add book to library 📚"""
self.books[book.isbn] = book
print(f"✅ Added: {book.emoji} {book.title}")
def register_member(self, member_id, name):
"""Register new member 👤"""
self.members[member_id] = {
"name": name,
"borrowed": [],
"fines": 0
}
def checkout_book(self, isbn, member_id):
"""Check out a book 📖"""
if isbn not in self.books:
raise ValueError("Book not found! 😔")
if member_id not in self.members:
raise ValueError("Member not registered! 🚫")
book = self.books[isbn]
member = self.members[member_id]
if not book.available:
return False
if len(member["borrowed"]) >= self.borrow_limit:
raise ValueError(f"Borrow limit reached! 📚 Max: {self.borrow_limit}")
book.available = False
book.borrowed_by = member_id
book.due_date = datetime.now() + timedelta(days=14)
member["borrowed"].append(isbn)
return True
def return_book(self, isbn, member_id):
"""Return a book 📚"""
book = self.books[isbn]
member = self.members[member_id]
if isbn not in member["borrowed"]:
raise ValueError("Book not borrowed by this member! 🤔")
# Calculate late fees
if datetime.now() > book.due_date:
days_late = (datetime.now() - book.due_date).days
fine = days_late * self.daily_fine
member["fines"] += fine
print(f"⚠️ Late fee: ${fine:.2f}")
book.available = True
book.borrowed_by = None
book.due_date = None
member["borrowed"].remove(isbn)
return True
def get_available_books(self):
"""Get all available books 📚"""
return [book for book in self.books.values() if book.available]
# 🧪 Test our library system
@pytest.fixture
def library():
"""Create test library 📚"""
lib = Library()
# Add test books
lib.add_book(Book("978-1", "Python Magic", "Guido", "technology"))
lib.add_book(Book("978-2", "Test Driven Life", "Kent Beck", "technology"))
lib.add_book(Book("978-3", "The Hobbit", "Tolkien", "fiction"))
# Register test members
lib.register_member("M001", "Alice")
lib.register_member("M002", "Bob")
return lib
def test_add_books(library):
# 📚 Test book addition
assert len(library.books) == 3
assert library.books["978-1"].title == "Python Magic"
assert library.books["978-3"].emoji == "🧙"
def test_checkout_book(library):
# 📖 Test checking out books
success = library.checkout_book("978-1", "M001")
assert success == True
assert library.books["978-1"].available == False
assert "978-1" in library.members["M001"]["borrowed"]
def test_borrow_limit(library):
# 🚫 Test borrowing limits
library.checkout_book("978-1", "M001")
library.checkout_book("978-2", "M001")
library.checkout_book("978-3", "M001")
# Try to borrow 4th book
new_book = Book("978-4", "Extra Book", "Author", "fiction")
library.add_book(new_book)
with pytest.raises(ValueError, match="Borrow limit reached"):
library.checkout_book("978-4", "M001")
def test_return_book_with_fine(library, monkeypatch):
# 💰 Test late fee calculation
library.checkout_book("978-1", "M001")
book = library.books["978-1"]
# Mock the book as 3 days overdue
book.due_date = datetime.now() - timedelta(days=3)
library.return_book("978-1", "M001")
assert library.members["M001"]["fines"] == 1.50 # 3 days * $0.50
def test_available_books(library):
# 🔍 Test finding available books
initial_available = len(library.get_available_books())
library.checkout_book("978-1", "M001")
library.checkout_book("978-2", "M002")
available = library.get_available_books()
assert len(available) == initial_available - 2
assert all(book.available for book in available)
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Write pytest tests with confidence 💪
- ✅ Use fixtures to manage test setup and teardown 🛡️
- ✅ Test exceptions and edge cases properly 🎯
- ✅ Apply mocking for external dependencies 🐛
- ✅ Create parametrized tests for multiple scenarios 🚀
Remember: Testing isn’t about finding bugs - it’s about building confidence in your code! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered pytest basics!
Here’s what to do next:
- 💻 Practice with the exercises above
- 🏗️ Add tests to your existing Python projects
- 📚 Explore pytest plugins like pytest-cov for coverage
- 🌟 Share your testing journey with others!
Remember: Every Python expert writes tests. Good tests make great developers! Keep testing, keep learning, and most importantly, have fun! 🚀
Happy testing! 🎉🚀✨