Работа с частичными моками в PHPUnit 10
В этом году должен выйти 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
}
}