Python · Тестирование с pytest · Начальный
Организация тестов
Структурируйте тестовые файлы и директории для удобного поиска и поддержки.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Обнаружение в действии: функции, классы и структура
#Краткий справочник: организация тестов
#Упражнения:
Реорганизация плоских тестов в классы
#В файле ниже есть 6 плоских тест-функций для двух тем (разворот строк и фильтрация списков). Реорганизуйте их в два класса `Test*` -- `TestReverse` и `TestFilter` -- не изменяя ни одного оператора assert.
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([]) == []
Решение
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([]) == []
Создание класса TestStringUtils
#Создайте класс `TestStringUtils` с тремя тест-методами: один для `capitalize_words(s)` (делает первую букву каждого слова заглавной), один для `count_vowels(s)` (считает a, e, i, o, u без учёта регистра) и один для `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 # добавьте три тест-метода здесь
Решение
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
Добавление setup_method для общего состояния
#Добавьте `setup_method` в класс `TestShoppingCart`, чтобы каждый тест получал свежую корзину. Корзина должна быть словарём `{'items': [], 'total': 0}`. Убедитесь, что `test_cart_still_empty_after_other_test` проходит даже после `test_can_add_item`.
class TestShoppingCart:
# добавьте setup_method здесь
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):
# должен проходить даже если test_can_add_item выполнился первым
assert self.cart['items'] == []
Решение
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'] == []
Разделение тестов по файлам и фильтрация через -k
#Создайте два тестовых файла: `test_math.py` с тестами для `add` и `multiply`, и `test_strings.py` с тестами для `str.upper()` и `str.lower()`. Запустите `pytest -k 'math'` и убедитесь, что выполняются только тесты для математических операций. Затем запустите `pytest -k 'multiply'`.
# test_math.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# напишите test_add и test_multiply здесь
# test_strings.py
# напишите test_upper и test_lower
Решение
# 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'
Создание структуры src/ + tests/
#Создайте минимальную структуру проекта: поместите `string_utils.py` с функцией `slugify(s)` (переводит в нижний регистр и заменяет пробелы на дефисы) в `src/`, поместите два теста в `tests/test_string_utils.py` и добавьте `pytest.ini` с `testpaths = tests`. Запустите pytest из корня проекта.
# src/string_utils.py
def slugify(s):
pass # нижний регистр + замена пробелов дефисами
# tests/test_string_utils.py
# импортируйте и протестируйте slugify здесь
# pytest.ini
# [pytest]
# testpaths = tests
Решение
# 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