Testing with pytest: Your Code’s Safety Net 🧪
The Story of the Careful Chef
Imagine you’re a chef in a busy restaurant kitchen. Every dish you make goes to a customer. What if you accidentally added salt instead of sugar to the cake? Disaster!
Smart chefs taste their food before serving. They check if the soup is too salty, if the pasta is cooked right, if the sauce tastes perfect.
pytest is your tasting spoon for code. Before your code goes to real users, pytest checks if everything works correctly. It catches mistakes before anyone else sees them!
1. Writing Test Functions
What Are Test Functions?
Think of test functions like little taste-testers. Each one checks ONE thing about your code.
The Rule: Your test function name must start with test_. That’s how pytest finds it!
# Your actual code (the dish)
def add(a, b):
return a + b
# Your test (the taste test)
def test_add_two_numbers():
result = add(2, 3)
assert result == 5
How It Works
graph TD A["Write your code"] --> B["Write a test function"] B --> C["Test name starts with test_"] C --> D["Run pytest"] D --> E{Pass or Fail?} E -->|Pass| F["Your code works!"] E -->|Fail| G["Fix and try again"]
Simple Example
# calculator.py
def multiply(x, y):
return x * y
# test_calculator.py
def test_multiply_positive():
assert multiply(3, 4) == 12
def test_multiply_zero():
assert multiply(5, 0) == 0
Key Points:
- One function = One specific check
- Name tells you what it tests
assertis the magic word that checks if something is true
2. Test Classes
Grouping Tests Together
Sometimes you have many tests for the same thing. Like having a whole checklist for one recipe!
Test classes group related tests. The class name must start with Test.
class TestCalculator:
def test_add(self):
assert add(1, 1) == 2
def test_subtract(self):
assert subtract(5, 3) == 2
def test_multiply(self):
assert multiply(2, 4) == 8
Why Use Classes?
graph TD A["TestCalculator Class"] --> B["test_add"] A --> C["test_subtract"] A --> D["test_multiply"] A --> E["test_divide"]
Benefits:
- Keep related tests together
- Share setup between tests
- Organized and easy to find
- Run all tests in a class at once
3. pytest Assertions
The assert Keyword
assert is like asking a yes/no question. If the answer is “yes” (True), the test passes. If “no” (False), it fails.
# Simple assertions
assert 5 > 3 # Is 5 greater than 3? YES!
assert "cat" == "cat" # Are these equal? YES!
assert 10 != 5 # Are these different? YES!
Common Assertion Patterns
def test_various_assertions():
# Check equality
assert 2 + 2 == 4
# Check if something is in a list
fruits = ["apple", "banana"]
assert "apple" in fruits
# Check if something is True/False
assert True
assert not False
# Check if value is None
x = None
assert x is None
pytest Gives Helpful Errors!
When a test fails, pytest shows exactly what went wrong:
assert 3 + 3 == 7
> 6 == 7
E False
It tells you: “You expected 7, but got 6!”
4. Fixtures
The Prep Kitchen
Before cooking, chefs prepare ingredients: chop vegetables, measure spices, preheat the oven.
Fixtures are your prep work for tests. They set up what your tests need.
import pytest
@pytest.fixture
def sample_list():
return [1, 2, 3, 4, 5]
def test_list_length(sample_list):
assert len(sample_list) == 5
def test_list_sum(sample_list):
assert sum(sample_list) == 15
How Fixtures Work
graph TD A["@pytest.fixture"] --> B["Creates sample_list"] B --> C["Passes to test function"] C --> D["Test uses it"] D --> E["Fixture cleaned up"]
Fixture with Setup and Cleanup
@pytest.fixture
def database_connection():
# SETUP: Before test runs
db = connect_to_database()
yield db # Give this to the test
# CLEANUP: After test finishes
db.close()
def test_save_user(database_connection):
database_connection.save(user)
assert True
The yield magic: Everything before yield is setup. Everything after is cleanup!
5. Parametrized Tests
Test Many Values at Once!
What if you want to test adding with 10 different number pairs? Write 10 test functions? No way!
Parametrize lets you run one test with many different inputs.
import pytest
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert add(a, b) == expected
This ONE function runs FOUR times with different values!
Visual: How It Runs
graph TD A["test_add parametrized"] --> B["Run 1: add#40;2,3#41; = 5"] A --> C["Run 2: add#40;0,0#41; = 0"] A --> D["Run 3: add#40;-1,1#41; = 0"] A --> E["Run 4: add#40;100,200#41; = 300"]
More Complex Example
@pytest.mark.parametrize("word, expected_length", [
("cat", 3),
("hello", 5),
("", 0),
("Python", 6),
])
def test_word_length(word, expected_length):
assert len(word) == expected_length
Super Efficient! One test function, many test cases.
6. Marks and Skipping Tests
Special Labels for Tests
Marks are like sticky notes on your tests. They tell pytest something special about each test.
Skip a Test
Sometimes a test isn’t ready yet, or only works on certain computers.
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(
sys.platform == "win32",
reason="Only runs on Linux"
)
def test_linux_only():
pass
Mark Tests as Expected to Fail
@pytest.mark.xfail(reason="Known bug #123")
def test_broken_feature():
assert broken_function() == "works"
xfail = “I expect this to fail.” pytest won’t count it as a failure.
Custom Marks
@pytest.mark.slow
def test_big_calculation():
# Takes 5 minutes to run
pass
@pytest.mark.database
def test_save_to_db():
pass
Run only specific marked tests:
pytest -m slow # Only slow tests
pytest -m "not slow" # Everything except slow
Mark Summary
graph TD A["Marks"] --> B["@pytest.mark.skip"] A --> C["@pytest.mark.skipif"] A --> D["@pytest.mark.xfail"] A --> E["@pytest.mark.custom_name"] B --> F["Always skip"] C --> G["Skip if condition true"] D --> H["Expected to fail"] E --> I["Your own categories"]
Running Your Tests
Basic Commands
# Run all tests
pytest
# Run with details
pytest -v
# Run one file
pytest test_calculator.py
# Run one test
pytest test_calculator.py::test_add
See the Results
test_calc.py::test_add PASSED
test_calc.py::test_subtract PASSED
test_calc.py::test_multiply FAILED
=============== 1 failed, 2 passed ===============
Green = Good! Red = Fix it!
Quick Reference
| Concept | Key Point |
|---|---|
| Test Functions | Start name with test_ |
| Test Classes | Start name with Test |
| Assertions | Use assert to check |
| Fixtures | @pytest.fixture for setup |
| Parametrize | Test many values at once |
| Skip | @pytest.mark.skip |
| Expected Fail | @pytest.mark.xfail |
Your Journey Forward
You now have a safety net for your code! Every time you write code, write tests too. It’s like having a friend who double-checks your work.
Remember: Good tests = Confident coding. You can change your code without fear because pytest will tell you if something breaks!
đź§Ş Happy Testing!
