Инверсия зависимостей — это крайне мощная техника, которая работает не только с функциями, но и с объектами. В этом уроке мы рассмотрим ее глубже на примере HTTP-запросов. Здесь мы познакомимся с таким понятием как стаб (от английского stub — заглушка).
Предположим, что у нас есть функция, которая анализирует приватные репозитории на GitHub одной конкретной организации. При этом функция возвращает форки — репозитории, отпочкованные от основного репозитория:
# Библиотека для работы с GitHub API
from github import Github
def get_private_fork_names(username):
    client = Github("access_token")
    # Клиент выполняет запрос на GitHub и возвращает
    # список приватных репозиториев указанной организации
    repos = client.get_user(username).get_repos(type="private")
    # Оставляем только имена приватных форков
    return [repo.name for repo in repos if repo.fork == True]
Давайте ее протестируем. Что мы хотим от этой функции? В первую очередь убедиться, что она работает правильно – возвращает массив имен приватных форков. Идеальный тест выглядел бы так:
def test_get_private_fork_names():
    names = get_private_fork_names("hexlet")
    assert names == [
        "python-project",
        "js-project",
        "go-project",
    ]  # ожидаемый список имен
К сожалению, не все так просто. Внутри функции выполняется HTTP-запрос. Обсудим, какие проблемы из-за этого могут возникнуть:
- Нестабильная сеть может тормозить выполнение тестов и приводить к фантомным ошибкам. Тесты будут иногда проходить, иногда нет
- У github.com и других подобных сервисов установлены временные ограничения на запросы — в секунду, в час и так далее. Скорее всего, тесты начнут упираться в эти лимиты. Есть вероятность, что GitHub заблокирует компьютер, с которого идут запросы
- Реальные данные на GitHub не статичны, они могут меняться. Это опять же приведет к ошибкам и необходимости править тесты
В этом примере HTTP-запрос воспринимается как помеха к тому, чтобы протестировать нашу основную логику. Мы доверяем PyGithub и его библиотеке github — то есть нам не нужно проверять, что она работает правильно.
Из предыдущего урока мы узнали о нескольких способах выхода из этой ситуации и теперь можем применить один из них.
Инверсия зависимостей
Чтобы использовать инверсию зависимостей, добавим вторым аргументом функции сам клиент github. Так мы сможем подменить его в тестах:
from github import Github
# Клиент для работы с Github передается снаружи
# теперь его можно подменять
def get_private_fork_names(username, client=Github()):
    # отстальная логика повторяется
    repos = client.get_user(username).get_repos(type="private")
    return [repo.name for repo in repos if repo.fork == True]
Нам нужно реализовать в тестах фейковый клиент, который ведет себя примерно так же, как и реальный Github. Разница только в том, что наш клиент не выполняет сетевых запросов.
Мы даже можем упростить задачу, и реализовать лишь тот функционал, что используется в тестах: get_user() и get_repos(). В таком случае мы сможем протестировать, что функция get_private_fork_names() работает корректно:
# Структура этого класса описывает только ту часть,
# которая необходима для вызова client.get_user и client.get_repos
class GithubFake:
    # Здесь мы описываем желаемые данные, которые должны вернуться в тесте
    def __init__(self, data):
        self.data = data
    # Возвращаем сами себя, чтобы иметь возможность вызвать get_repos() по цепочке
    def get_user(self, name):
        return self
    # Возвращаем переданные в начале данные
    def get_repos(self):
        return self.data
Посмотрим на сам тест с использованием этого клиента:
from github_fake import GithubFake
def test_get_private_fork_names():
    class FakeRepo:
        def __init__(self, name, fork):
            self.name = name
            self.fork = fork
    # Создаем фейковые данные
    test_data = [
        FakeRepo("repo1", True),
        FakeRepo("repo2", False),
        FakeRepo("repo3", True),
    ]
    client = GithubFake(test_data)
    names = get_private_fork_names("hexlet", client=client)
    assert names == ["repo1", "repo3"]  # ожидаемый список имен
В тестировании подобные фейковые объекты или функции используются часто. У них есть специальное название – стаб.
Стаб заменяет реальный объект или функцию, чтобы мы могли избежать побочных эффектов или сделать код детерминированным. Стаб не используется для проверки чего-либо — он лишь позволяет изолировать ту часть, которая мешает тестированию основной логики.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.