Python · Testing with pytest · Beginner
Organizing Tests
Structure test files and directories for discoverability and maintainability.
Quick topic start and explanations before exercises (exercises below):
Discovery in Action: Functions, Classes, and Layout
#Organization Quick Reference
#Exercises:
Reorganize Flat Tests into Classes
#The file below has 6 flat test functions for two topics (string reversal and list filtering). Reorganize them into two `Test*` classes — `TestReverse` and `TestFilter` — without changing any assert statements.
def reverse(s):
return s[::-1]
def filter_positive(nums):
return [n for n in nums if n > 0]
def test_reverse_hello():
assert reverse('hello') == 'olleh'
def test_reverse_empty():
assert reverse('') == ''
def test_reverse_single():
assert reverse('a') == 'a'
def test_filter_mixed():
assert filter_positive([1, -2, 3, -4]) == [1, 3]
def test_filter_all_negative():
assert filter_positive([-1, -2]) == []
def test_filter_empty():
assert filter_positive([]) == []
Solution
def reverse(s):
return s[::-1]
def filter_positive(nums):
return [n for n in nums if n > 0]
class TestReverse:
def test_hello(self):
assert reverse('hello') == 'olleh'
def test_empty(self):
assert reverse('') == ''
def test_single_char(self):
assert reverse('a') == 'a'
class TestFilter:
def test_mixed_numbers(self):
assert filter_positive([1, -2, 3, -4]) == [1, 3]
def test_all_negative(self):
assert filter_positive([-1, -2]) == []
def test_empty_list(self):
assert filter_positive([]) == []
Create a TestStringUtils Class
#Create a `TestStringUtils` class with three test methods: one for `capitalize_words(s)` (capitalizes each word), one for `count_vowels(s)` (counts a, e, i, o, u case-insensitively), and one for `is_palindrome(s)`.
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
def count_vowels(s):
return sum(1 for c in s.lower() if c in 'aeiou')
def is_palindrome(s):
return s == s[::-1]
class TestStringUtils:
pass # add three test methods here
Solution
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
def count_vowels(s):
return sum(1 for c in s.lower() if c in 'aeiou')
def is_palindrome(s):
return s == s[::-1]
class TestStringUtils:
def test_capitalize_words(self):
assert capitalize_words('hello world') == 'Hello World'
assert capitalize_words('python') == 'Python'
def test_count_vowels(self):
assert count_vowels('hello') == 2
assert count_vowels('rhythm') == 0
assert count_vowels('AEIOU') == 5
def test_is_palindrome(self):
assert is_palindrome('racecar') is True
assert is_palindrome('hello') is False
assert is_palindrome('') is True
Add setup_method for Shared State
#Add `setup_method` to the `TestShoppingCart` class so each test gets a fresh cart. The cart should be a dict `{'items': [], 'total': 0}`. Verify that `test_cart_still_empty_after_other_test` passes even when run after `test_can_add_item`.
class TestShoppingCart:
# add setup_method here
def test_cart_starts_empty(self):
assert self.cart['items'] == []
def test_total_starts_at_zero(self):
assert self.cart['total'] == 0
def test_can_add_item(self):
self.cart['items'].append('apple')
assert len(self.cart['items']) == 1
def test_cart_still_empty_after_other_test(self):
# this must pass even if test_can_add_item ran first
assert self.cart['items'] == []
Solution
class TestShoppingCart:
def setup_method(self):
self.cart = {'items': [], 'total': 0}
def test_cart_starts_empty(self):
assert self.cart['items'] == []
def test_total_starts_at_zero(self):
assert self.cart['total'] == 0
def test_can_add_item(self):
self.cart['items'].append('apple')
assert len(self.cart['items']) == 1
def test_cart_still_empty_after_other_test(self):
assert self.cart['items'] == []
Split Tests Across Files and Filter with -k
#Create two test files: `test_math.py` with tests for `add` and `multiply`, and `test_strings.py` with tests for `str.upper()` and `str.lower()`. Run `pytest -k 'math'` and verify only the math tests run. Then run `pytest -k 'multiply'`.
# test_math.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# write test_add and test_multiply here
# test_strings.py
# write test_upper and test_lower
Solution
# test_math.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_multiply():
assert multiply(3, 4) == 12
assert multiply(0, 5) == 0
# test_strings.py
def test_upper():
assert 'hello'.upper() == 'HELLO'
def test_lower():
assert 'WORLD'.lower() == 'world'
Create a src/ + tests/ Layout
#Create a minimal project layout: put `string_utils.py` with a `slugify(s)` function (lowercases and replaces spaces with dashes) inside `src/`, put two tests inside `tests/test_string_utils.py`, and add `pytest.ini` with `testpaths = tests`. Run pytest from the project root.
# src/string_utils.py
def slugify(s):
pass # lowercase + replace spaces with dashes
# tests/test_string_utils.py
# import and test slugify here
# pytest.ini
# [pytest]
# testpaths = tests
Solution
# src/string_utils.py
def slugify(s):
return s.lower().replace(' ', '-')
# tests/test_string_utils.py
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from string_utils import slugify
def test_slugify_spaces():
assert slugify('hello world') == 'hello-world'
def test_slugify_uppercase():
assert slugify('Hello World') == 'hello-world'
# pytest.ini
# [pytest]
# testpaths = tests