Зарегистрируйтесь, чтобы продолжить обучение

Тестирование HTTP-запросов PHP: Продвинутое тестирование

Инверсия зависимостей крайне мощная техника, которая работает не только с функциями, но и с объектами. Рассмотрим её глубже на примере HTTP-запросов и познакомимся с таким понятием как "заглушка" (stub).

Предположим, что у нас есть функция, которая анализирует приватные репозитории организации на GitHub и возвращает те, что являются форками (репозитории "отпочкованные" от основного репозитория):

<?php

// Библиотека для работы с GitHub API
// https://github.com/KnpLabs/php-github-api
use Github\Client;

function getForkedRepositories($org)
{
    $client = new \Github\Client();
    // Возвращает список репозиториев пользователя/организации
    $repositories = $client->api('user')->repositories($org);
    return array_filter($repositories, fn($repository) => $repository['fork']);
};

Давайте её протестируем. Что мы хотим от этой функции? В первую очередь убедиться, что она работает правильно – возвращает массив форков у конкретного пользователя. Идеальный тест выглядел бы так:

<?php

class GithubTest extends TestCase
{
    public function testGetForkedRepositories(): void
    {
        $forks = getForkedRepositories('hexlet');
        $this->assertEquals([/* массив репозиториев */], $forks)
    }
}

К сожалению, не всё так просто. Внутри функции выполняется HTTP-запрос. Прикинем, какие проблемы из-за этого могут возникнуть:

  1. Нестабильная сеть может тормозить выполнение тестов и приводить к "фантомным" ошибкам. Тесты будут иногда проходить, иногда нет.
  2. У сервисов подобных github.com установлены ограничения на запросы в секунду, в час, день и так далее. Со 100% вероятностью тесты начнут упираться в эти лимиты. Более того, есть шанс что машина с которой идут запросы, будет заблокирована.
  3. Реальные данные на GitHub не статичны, они могут и, скорее всего, будут меняться, что опять же приведет к ошибкам и необходимости править тесты.

В данном примере HTTP-запрос воспринимается как помеха к тому, чтобы протестировать нашу основную логику. Мы доверяем github.com и его библиотеке KnpLabs/php-github-api, то есть нам не нужно проверять, что она работает правильно (иначе можно свихнуться, если не доверять никому).

Из предыдущего урока мы узнали о нескольких способах выхода из этой ситуации и теперь можем применить один из них.

Инверсия зависимостей

Для использования инверсии зависимости добавим вторым аргументом функции сам клиент библиотеки. Это позволит подменить его в тестах:

<?php

use Github\Client;

function getForkedRepositories($org, $client = null)
{
    // Создаем по умолчанию чтобы не усложнять основной вариант использования
    $client = $client ?? new \Github\Client();
    // Возвращает список репозиториев пользователя/организации
    $repositories = $client->api('user')->repositories($org);
    return array_filter($repositories, fn($repository) => $repository['fork']);
};

Теперь в тестах можно выполнить подмену реализации клиента. Для этого нам понадобится фейковый объект. Создать подобный объект можно двумя способами: либо описывать полноценный класс, либо использовать встроенный в PHPUnit генератор фейковых объектов (заглушек, stubs). Последний способ является предпочтительным, поэтому разберем его. Пример создания заглушки:

<?php

class StubTest extends TestCase
{
    public function testStub()
    {
        // Создать заглушку для класса SomeClass
        $stub = $this->createMock(SomeClass::class);

        // Настроить заглушку
        $stub->method('doSomething')
            ->willReturn('foo');

        // Вызов $stub->doSomething() теперь вернёт 'foo'
        $this->assertSame('foo', $stub->doSomething());
    }
}

Метод createMock() создает объект переданного класса или интерфейса, но с некоторыми оговорками. Во время создания, у такого объекта не вызывается конструктор, а все методы становятся пустыми и возвращают null. Такой реализации уже может быть достаточно для подмены.

Если получившийся объект наполняется дополнительной логикой, то в примере выше к заглушке добавляется метод doSomething(), который всегда возвращает строку foo. createMock() создает заглушки не только на основе классов, но и на основе интерфейсов. Для этого внутри создается временный класс реализующий данный интерфейс. Все это возможно благодаря Reflection Api.

Попробуем создать заглушку для тестирования функции getForkedRepositories(). Главная сложность здесь состоит в том, что клиент содержит цепочку из двух вызовов api()->repositories(). PHPUnit позволяет эмулировать и такое поведение, но чуть более сложной конфигурацией:

<?php

$stub = $this->createMock(\Github\Client::class);
// Указываем что метод api() возвращает саму заглушку
$stub->method('api')
    ->willReturn($this->returnSelf());
$stub->method('repositories')
    ->will(/* данные которые мы хотим передать */);

И сам тест с использованием этой заглушки:

<?php

class GithubTest extends TestCase
{
    public function testGetForkedRepositories(): void
    {
        $stub = $this->createMock(\Github\Client::class);
        // Указываем что метод api() возвращает саму заглушку
        $stub->method('api')
            ->willReturn($this->returnSelf());
        $stub->method('repositories')
            ->will([
                ['name' => 'guzzle', 'fork' => true],
                ['name' => 'github-client', 'fork' => false]
            ]);

        // Внутри выполняется "запрос", который возвращает сформированные выше данные
        $org = /* имя пользователя на гитхабе */;
        $forks = getForkedRepositories($org, $stub);
        $this->assertEquals([/* массив репозиториев */], $forks)
    }
}

В тестировании для подобных заглушек есть специальное название – стаб (stub). Стаб заменяет реальный объект или функцию, позволяя избежать выполнения побочных эффектов или сделать код детерминированным. Стаб не используется для проверки чего-либо, он лишь позволяет изолировать ту часть, которая "мешает" тестированию основной логики.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»