Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Инверсия зависимостей PHP: Продвинутое тестирование

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

Представьте себе функцию, которая регистрирует пользователя. Она создаёт запись в базе данных и отправляет приветственное письмо:

<?php

$params = [
    'email' => 'lala@example.com',
    'password' => 'qwerty',
];
registerUser($params);

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

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

<?php

// Гипотетический пример
class RegistrationTest extends TestCase
{
    private $connect;

    public function setUp(): void
    {
        $this->connect = /* connect to db */;
        $this->connect->beginTransaction();
    }

    public function testRegisterUser(): void
    {
        // Внутри идет добавление данных в базу
        $id = registerUser(['name' => 'Mike']);
        $user = User::find($id);
        $this->assertEquals('Mike', $user->name);
    }

    public function tearDown(): void
    {
        // За счет отката база возвращается в исходное состояние
        $this->connect->rollbackTransaction();
    }
}

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

<?php

use EmailSender;

function registerUser($params)
{
    $sender = new EmailSender();
    $user = new User($params);
    if ($user->save()) {
        // Отправляет письмо об успешной регистрации пользователю $user
        $sender->send('registration', ['user' => $user]);
        return true;
    }
    return false;
}

Существует несколько подходов, позволяющих отключить отправку в тестах. Самый простой — переменная окружения, которая показывает среду выполнения:

<?php

// Выполняем этот код только если мы не в тестовой среде
if ($_ENV['ENV'] !== 'test') {
    $sender->send('registration', ['user' => $user]);
}

Несмотря на простоту использования, такой подход считается плохой практикой. Формально, из-за него происходит нарушение абстракции, код начинает знать о том, где он выполняется. Со временем таких проверок становится всё больше и код становится грязнее. Более того, если нам всё же надо убедиться, что письмо отправляется (с правильными данными!), то мы не сможем этого сделать.

Следующий способ – поддержка режима тестирования внутри самой библиотеки. Например, где-нибудь на этапе инициализации тестов можно сделать так:

<?php

use EmailSender;

// В свою очередь метод send проверяет значение этого свойства
EmailSender::$test = true;

Теперь в любом другом месте, где импортируется и используется метод send(), реальная отправка происходить не будет:

<?php

// Ничего не происходит
$sender->send('registration', ['user' => $user]);
// В отличие от первого варианта, прикладной код ни о чем не догадывается

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

Что делать, если используемая библиотека не поддерживает режим тестирования? Существует ещё один, наиболее универсальный способ. Он основан на применении инверсии зависимостей. Программу можно изменить так, чтобы она вызывала метод send() не напрямую, а принимала её как параметр:

<?php

use EmailSenderInterface;

function registerUser($params, EmailSenderInterface $sender)
{
    $user = new User($params);
    if ($user->save()) {
        $sender->send('registration', ['user' => $user]);
        return $user->id;
    }

    return false;
}

И тест:

<?php

public function testRegisterUser()
{
    $sender = new FakeEmailSender(); // Реализация для тестов
    // Отправки письма не происходит
    $id = registerUser(['name' => 'Mike'], $sender);
    $user = User::find($id);
    $this->assertEquals('Mike', $user);
}

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

Теперь плюсы. Ни библиотека, ни код ничего не знают про тесты. Этот способ наиболее гибкий, он позволяет задавать конкретное поведение для конкретной ситуации. В некоторых экосистемах инверсия зависимостей определяет процесс сборки приложения. Особенно в мире PHP, Java и C#.


Дополнительные материалы

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

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

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

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

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

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

Об обучении на Хекслете

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы PHP-разработчик
Профессия
Разработка веб-приложений на Laravel
25 мая 10 месяцев

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

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

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

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»