Как спроектировать функцию для легкого юнит-тестирования

Ответ

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

  1. Чистота (Purity): Функция не должна иметь побочных эффектов (не изменять глобальные переменные, не писать в файлы/БД). Её результат должен зависеть только от входных аргументов.

  2. Принцип единственной ответственности (Single Responsibility): Функция должна выполнять одну, чётко определённую задачу. Это упрощает её понимание и тестирование.

  3. Явные зависимости (Explicit Dependencies): Всё, что нужно функции для работы (данные, сервисы, конфигурации), должно передаваться ей в качестве аргументов. Этот подход называется Dependency Injection.

Сравнение подходов

Плохой пример (неявная зависимость от БД): Функция жестко связана с внешним источником данных, что делает её тестирование сложным. Для проверки логики потребуется реальное или тестовое подключение к базе.

# Трудно тестировать, так как функция связана с БД
def get_user_report():
    db_connection = connect_to_database()
    user_data = db_connection.fetch_user_data()
    # ...сложная логика формирования отчета
    return f"Report for {user_data['name']}"

Хороший пример (зависимость передается явно): Функция получает уже готовые данные. Её можно протестировать в полной изоляции, передав любой тестовый словарь.

# Легко тестировать, передавая любые данные
def generate_user_report(user_data: dict) -> str:
    # ...сложная логика формирования отчета
    return f"Report for {user_data['name']}"

Пример теста для хорошей функции

import unittest

def test_generate_user_report():
    mock_user = {"name": "Alice", "id": 1}
    report = generate_user_report(mock_user)
    assert "Report for Alice" in report

Такой подход позволяет в тестах подменять реальные зависимости (БД, API) на тестовые заглушки (моки), изолируя и проверяя только логику самой функции.

Ответ 18+ 🔞

А, ну ты смотри, какие тут умные мысли про тестирование функций подъехали! Слушай, а ведь реально, если подумать — всё это про то, чтобы не наступать на одни и те же грабли, на которые мы все уже сто раз наступали, блядь.

Вот смотри, чтобы твои функции не превращались в непролазное говно, которое потом тестировать — только волосы на жопе рвать, есть несколько простых, как три копейки, правил.

Первое — чистота, ёпта! Это когда твоя функция ведёт себя как примерный школьник: не гадит в глобальные переменные, не пишет тайком в базу данных, и её результат зависит только от того, что ты ей в рот сунул. Как калькулятор, блядь. Дал 2 и 2 — получи 4. Всегда. А не «сегодня 4, а завтра, потому что глобальная переменная is_friday поменялась, будет 5». Это пиздец, а не функция.

Второе — одна задача на рыло! Не надо делать из функции швейцарский нож, который и бутерброд намажет, и в космос слетает. Сделал одну чёткую вещь — и свободен. И тестировать её — раз плюнуть.

И третье, самое важное, блядь — явные зависимости! Это вообще святое. Если твоей функции для работы нужны данные или какой-нибудь сервис — ты их ей передаёшь, а не заставляешь её самой лезть в тёмный чулан мировой базы данных и там на ощупь искать. Этот подход умные дяди называют «инъекцией зависимостей», звучит страшно, а суть — проще пареной репы.

Давай на живых примерах, а то нихуя не понятно

Вот смотри, как делать НЕ НАДО (пиздец как не надо):

# Функция-одиночка, которая сама всё знает и умеет. И тестировать её — сплошное геморройное веселье.
def get_user_report():
    db_connection = connect_to_database() # Опа, полезла в базу! А если её нет?
    user_data = db_connection.fetch_user_data() # А если тут исключение?
    # ...тут ещё наверняка куча сложной логики
    return f"Report for {user_data['name']}"

Представляешь, чтобы проверить, правильно ли она имя в отчёт подставляет, тебе надо разворачивать целую базу данных, накатывать туда фикстуры… Да ебать её в сраку, проще новую функцию написать! Это же пиздопроебищно!

А вот как надо — красиво и правильно:

# Функция-интроверт. Ей похуй на весь мир. Дал данные — она отработала.
def generate_user_report(user_data: dict) -> str:
    # ...вся та же сложная логика, но теперь она работает с тем, что дали
    return f"Report for {user_data['name']}"

Вот это — красота! Ей вообще по барабану, откуда эти user_data взялись. Из базы, из файла, с потолка, из твоей больной фантазии — ей всё равно. Она свою работу сделала. И протестировать её — ебушки-воробушки!

Пример теста, который пишешь за две секунды и он просто работает

import unittest

def test_generate_user_report():
    # Подсовываем функции фейковые, тестовые данные
    mock_user = {"name": "Alice", "id": 1} # Вот тебе, функция, «Алиса»! Работай!
    report = generate_user_report(mock_user)
    # Проверяем, не обосралась ли она
    assert "Report for Alice" in report

И всё! Никаких баз, никаких сетей, никакой магии. Всё под контролем. Такой подход позволяет подменять реальные, тяжёлые штуки (типа БД или внешних API) на лёгкие, как пух, заглушки (моки) и проверять только логику функции. А не то, как она там коннектится к чёрту на рога.

Короче, суть в чём: пиши функции так, будто завтра их будет тестировать твой злейший враг. И тогда даже он нихуя не придерется. Ну, почти.