Независимые проверки сходятся к модулю программы и показывают успешные и ошибочный результаты
Программирование

Автоматические тесты Python: первые шаги с unittest

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

Содержание

После изменения функции легко проверить один удачный пример вручную и пропустить сломанный крайний случай. Автоматический тест запускает заранее описанный сценарий и сравнивает фактический результат с ожидаемым. Если поведение изменилось, проверка сообщает об этом до публикации скрипта.

Тестовый случай — один проверяемый сценарий. Утверждение, или assertion, сравнивает результат с ожиданием. Фикстура — подготовленные данные и окружение, которые нужны тесту. Эти термины описывают действия, а не отдельную сложную систему.

Модуль unittest входит в Python. Начнём с функции из отдельного модуля, а работу с ожидаемыми ошибками свяжем с уроком об исключениях.

Отделите вычисление от ввода и вывода

Создайте пустой каталог проекта и файл report.py. Функция получает уже прочитанную строку CSV и возвращает новый словарь; файлов и сети у неё нет:

def normalize_row(row):
    url = row.get("url", "").strip()
    if not url:
        raise ValueError("Поле url обязательно")

    try:
        status = int(row["status"])
        elapsed_ms = int(row["elapsed_ms"])
    except (KeyError, TypeError, ValueError) as error:
        raise ValueError("status и elapsed_ms должны быть целыми числами") from error

    return {
        "url": url,
        "status": status,
        "elapsed_ms": elapsed_ms,
    }

Такая функция называется чистой в практическом смысле: результат зависит от аргумента, а вызов не меняет внешние файлы. Её легче тестировать, чем команду, которая одновременно читает CSV, печатает строки и удаляет старый отчёт.

Проверьте обычный результат

Создайте рядом файл test_report.py. Префикс test_ позволяет стандартному механизму обнаружить файл автоматически:

import unittest

from report import normalize_row


class NormalizeRowTests(unittest.TestCase):
    def test_converts_numbers_and_trims_url(self):
        source = {
            "url": "  https://example.com  ",
            "status": "200",
            "elapsed_ms": "184",
        }

        result = normalize_row(source)

        self.assertEqual(
            result,
            {
                "url": "https://example.com",
                "status": 200,
                "elapsed_ms": 184,
            },
        )

Запустите python -m unittest -v из каталога, где лежат оба файла. В выводе появится имя теста и ok. Команда импортирует тестовый модуль и вызывает методы, имя которых начинается с test.

Внутри метода сначала подготовлены данные, затем вызвана функция, после чего результат сравнивается с ожиданием. Эту последовательность часто называют Arrange, Act, Assert: подготовить, выполнить, проверить. Запоминать английские слова необязательно; важно не смешивать три этапа в неразборчивый блок.

Проверьте ожидаемую ошибку

Пустой URL должен приводить к ValueError. Добавьте второй метод внутрь того же класса:

    def test_rejects_empty_url(self):
        source = {
            "url": "   ",
            "status": "200",
            "elapsed_ms": "184",
        }

        with self.assertRaisesRegex(ValueError, "url обязательно"):
            normalize_row(source)

Повторите python -m unittest -v. Теперь должны пройти два теста. Контекст assertRaisesRegex проверяет и тип исключения, и значимую часть сообщения. Если функция молча примет пустой адрес или вызовет другой тип ошибки, тест завершится неуспешно.

Не проверяйте полный текст системного исключения, если он зависит от версии Python или операционной системы. Стабильной частью должна быть ваша формулировка, которая описывает нарушенное правило.

Используйте временный каталог для работы с файлами

Когда функция обязана читать файл, тест не должен писать в настоящую папку пользователя. Добавьте в report.py небольшую функцию:

from pathlib import Path


def read_first_line(path):
    return Path(path).read_text(encoding="utf-8").splitlines()[0]

В начало test_report.py добавьте импорт TemporaryDirectory, а в класс — ещё один тест:

from pathlib import Path
from tempfile import TemporaryDirectory


    def test_reads_first_line_from_temporary_file(self):
        with TemporaryDirectory() as directory:
            path = Path(directory) / "report.txt"
            path.write_text("первая\nвторая\n", encoding="utf-8")

            result = read_first_line(path)

            self.assertEqual(result, "первая")

Дополните импорт из report: from report import normalize_row, read_first_line. Временный каталог существует только внутри блока with и удаляется после него. Тест не трогает data/report.csv из предыдущих уроков и одинаково начинает каждый запуск.

Если проверяемая функция удаляет или перезаписывает файлы, направляйте её только во временный каталог и отдельно проверяйте, какие пути изменились. Реальную домашнюю папку, VPS или сетевой ресурс модульный тест использовать не должен.

Убедитесь, что тест действительно способен упасть

Временно измените ожидаемый статус в первом тесте с 200 на 201 и запустите команду снова. unittest должен показать FAIL и различие словарей. Верните 200 и убедитесь, что все три теста снова проходят.

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

Держите каждый сценарий независимым

Один тест не должен готовить данные для следующего. Порядок запуска может измениться, а неудача первого сценария не должна ломать остальные. Общую неизменяемую подготовку можно вынести в setUp, но для двух строк понятнее оставить данные рядом с проверкой.

Сеть, текущее время и случайные значения делают результат нестабильным. На начальном этапе вынесите их за границу тестируемой функции и передавайте готовые значения. Интеграционные проверки реального HTTP понадобятся позже, но они не заменяют быстрые тесты логики.

Официальный справочник unittest перечисляет утверждения, фикстуры и правила обнаружения. В следующем уроке программа впервые получит внешние данные через HTTP стандартной библиотекой.

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Что проверяет assertEqual?
02Для чего используется assertRaises?
03Что делает команда python -m unittest -v?

Разбираем коротко

Частые вопросы

Нужно ли устанавливать unittest через pip?

Нет. unittest входит в стандартную библиотеку Python.

Что лучше тестировать сначала?

Начните с функции без сети и реальных файлов: передайте ей обычные значения и проверьте возвращаемый результат или ожидаемое исключение.

Почему тесты используют временный каталог?

Он изолирует проверку от пользовательских файлов и автоматически удаляется после завершения, поэтому повторный запуск начинается с чистого состояния.