Разработка через тестирование на Python: от основ до продвинутых сценариев

Раздел: Тестирование -> Методология TDD

Основы методологии 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, тест проходит. Затем рефакторинг.

Типичная ошибка: написание слишком большого теста, проверяющего много утверждений сразу. TDD лучше работает с маленькими атомарными тестами.

Как использовать 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)
    
Ошибка: забыть импортировать pytest или неправильно указать match.

Как использовать 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
    
Распространенная проблема: фикстуры с большой областью видимости (scope) могут вызывать конфликты состояния.

Общие ошибки при 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 автоматически генерирует случайные целые числа для проверки свойств.

Разработка на основе тестирования (TDD) в Python - comments

En
Python разработка на основе тестирования (python)