В этом году должен выйти PHPUnit 10 (выпуск релиза планировался 2 апреля 2021 года, но был отложен). Если посмотреть на список изменений, то бросается в глаза большое количество удалений устаревшего кода. Одним из таких изменений является удаление метода MockBuilder::setMethods(), который активно использовался при работе с частичными моками. Этот метод не рекомендуется использовать уже с версии 8.0, но тем не менее он описан в документации без каких-либо альтернатив или упоминаний о его нежелательности. Если почитать исходники PHPUnit, issues и пулл реквесты на Github, то станет понятно, почему это было сделано и какие есть альтернативы.

В этой статье я расскажу про этот нюанс для тех, кто не обращал на него внимания раньше: расскажу про частичные моки, проблемы, возникающие при работе с setMethods, пути их решения, а также затрону вопрос миграции тестов на PHPUnit 10.

Что такое частичные моки?

У программного кода, который мы пишем, чаще всего есть какие-то зависимости.

При написании юнит-тестов мы изолируем эти зависимости, подставляя вместо реальных объектов какие-то заглушки с заранее известным состоянием. Это позволяет проверять работу только одного кусочка кода в один момент времени. Эти заглушки чаще всего реализуются с помощью моков.

Про название "мок"

У этого термина есть несколько названий на русском (мок, mock-объект, подставной объект, имитация). Дальше в тексте я буду пользоваться "калькой" английского слова mock (“мок”).

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

PHPUnit содержит встроенный механизм для работы с моками (документация). Одной из его возможностей является создание так называемых частичных моков (partial mocks), когда исходное поведение класса заменяется не полностью, а только для отдельных методов. Такие моки очень удобно использовать, когда вы хотите написать тест, который проверяет работу конкретного метода, но который в процессе своей работы вызывает другие методы (которые вы проверять не хотите).

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

Вот код базового класса, реализующий паттерн "команда":

abstract class AbstractCommand
{
    /**
     * @throws \PhpUnitMockDemo\CommandException
     * @return void
     */
    abstract protected function execute(): void;

    public function run(): bool
    {
        $success = true;
        try {
            $this->execute();
        } catch (\Exception $e) {
            $success = false;
            $this->logException($e);
        }

        return $success;
    }

    protected function logException(\Exception $e)
    {
        // Logging
    }
}