Разработка через тестирование на Python: от основ до продвинутых сценариев
Основы методологии TDD в Python
Разработка на основе тестирования (TDD) предполагает написание тестов до реализации кода. Цикл: написать тест (красный), сделать его проходящим (зеленый), рефакторинг. Далее рассмотрим основной подход и варианты.
Как реализовать базовый цикл TDD для функции сложения с использованием unittest?
Используется встроенный модуль unittest. Цель: убедиться, что функция add(a, b) возвращает сумму.
import unittest
def add(a, b):
return a + b
class TestAddition(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, 1), 0)
def test_add_zero(self):
self.assertEqual(add(0, 0), 0)
if __name__ == '__main__':
unittest.main()
Python разработка на основе тестирования (разработка на основе тестирования (tdd) в python)
После написания теста он падает, так как функция не определена. Затем реализуем add, тест проходит. Затем рефакторинг.
Как использовать pytest для упрощения синтаксиса тестов?
pytest позволяет писать тесты без создания классов. Пример:
def test_add_positive():
assert add(2, 3) == 5
Запуск: pytest test_file.py. pytest автоматически находит функции test_.
Как тестировать исключения с помощью pytest.raises?
Необходимо проверить, что функция возбуждает исключение при некорректных аргументах.
import pytest
def divide(a, b):
if b == 0:
raise ValueError('Деление на ноль')
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match='Деление на ноль'):
divide(10, 0)
Как использовать mock для изоляции внешних зависимостей?
Модуль unittest.mock позволяет подменить вызовы API или базы данных. Цель: тестировать логику без реальных вызовов.
from unittest.mock import MagicMock
def fetch_data(api_client):
return api_client.get('data')
def test_fetch_data():
mock_client = MagicMock()
mock_client.get.return_value = {'key': 'value'}
result = fetch_data(mock_client)
assert result == {'key': 'value'}
mock_client.get.assert_called_once_with('data')
В pytest можно использовать встроенный monkeypatch.
Как применить параметризацию тестов в pytest?
Параметризация позволяет запускать один тест с разными наборами данных.
@pytest.mark.parametrize('a,b,expected', [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0)
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
Это сокращает повторение кода и улучшает покрытие.
Как организовать тесты с фикстурами в pytest для подготовки данных?
Фикстуры создают и очищают ресурсы.
import pytest
@pytest.fixture
def sample_list():
return [1, 2, 3]
def test_sum(sample_list):
assert sum(sample_list) == 6
Общие ошибки при TDD:
- Тестирование слишком большого объема функциональности одним тестом.
- Отсутствие тестов на граничные случаи (пустые списки, нулевые значения).
- Забывают запускать тесты после рефакторинга.
- Использование хардкода вместо assertAlmostEqual для чисел с плавающей точкой.
Продвинутые примеры тестирования в TDD
Пример 1: Тестирование HTTP-запроса с mock и pytest
import requests
import pytest
from unittest.mock import patch
def get_user_name(user_id):
response = requests.get(f'https://api.example.com/users/{user_id}')
return response.json()['name']
@patch('requests.get')
def test_get_user_name(mock_get):
mock_get.return_value.json.return_value = {'name': 'Alice'}
result = get_user_name(1)
assert result == 'Alice'
mock_get.assert_called_once_with('https://api.example.com/users/1')
Результат: тест проходит, внешний запрос не выполняется.
$ pytest test_user.py -v
test_get_user_name PASSED
Пример 2: Тестирование работы с файлом через временные фикстуры
import pytest
import tempfile
import os
def read_file(path):
with open(path, 'r') as f:
return f.read()
@pytest.fixture
def temp_file():
file = tempfile.NamedTemporaryFile(delete=False)
file.write(b'Hello, TDD!')
file.close()
yield file.name
os.unlink(file.name)
def test_read_file(temp_file):
content = read_file(temp_file)
assert content == 'Hello, TDD!'
Фикстура создает временный файл, тест его читает, после завершения теста файл удаляется.
Пример 3: Параметризация с несколькими аргументами и тестирование исключений
import pytest
def divide(a, b):
if b == 0:
raise ZeroDivisionError('деление на ноль')
return a / b
@pytest.mark.parametrize('a,b,expected', [
(10, 2, 5),
(9, 3, 3),
(1, 3, 1/3)
])
def test_divide_success(a, b, expected):
assert divide(a, b) == pytest.approx(expected)
@pytest.mark.parametrize('a,b', [
(10, 0),
(0, 0)
])
def test_divide_zero_division(a, b):
with pytest.raises(ZeroDivisionError):
divide(a, b)
Используется pytest.approx для сравнения чисел с плавающей точкой.
Пример 4: Тестирование генераторов и состояний
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
def test_fibonacci():
gen = fibonacci(5)
assert list(gen) == [0, 1, 1, 2, 3]
# проверка, что генератор исчерпан
with pytest.raises(StopIteration):
next(gen)
Пример 5: Использование property-based testing с Hypothesis
from hypothesis import given, strategies as st
def add(a, b):
return a + b
@given(st.integers(), st.integers())
def test_add_commutative(a, b):
assert add(a, b) == add(b, a)
Hypothesis автоматически генерирует случайные целые числа для проверки свойств.