В этом году должен выйти PHPUnit 10 (релиз планировался на 2 апреля 2021 года, но был отложен). Если посмотреть на список изменений, то бросается в глаза большое количество удалений устаревшего кода. Одним из таких изменений является удаление метода MockBuilder::setMethods(), который активно использовался при работе с частичными моками. Этот метод не рекомендуется использовать с версии 8.0, но тем не менее он описан в документации без каких-либо альтернатив и упоминания о его нежелательности. Если почитать исходники PHPUnit, issues и пул-реквесты на GitHub, то станет понятно, почему так и какие есть альтернативы. В этой статье я освещу этот нюанс для тех, кто не обращал на него внимания раньше: расскажу про частичные моки, проблемы, возникающие при работе с setMethods, пути их решения, а также затрону вопрос миграции тестов на PHPUnit 10. У программного кода, который мы пишем, чаще всего есть какие-то зависимости. При написании юнит-тестов мы изолируем эти зависимости, подставляя вместо реальных объектов какие-то заглушки с заранее известным состоянием. Это позволяет проверять работу только одного кусочка кода в один момент времени. Эти заглушки чаще всего реализуются с помощью моков. У этого термина в русском языке есть несколько обозначений: мок, mock-объект, подставной объект, имитация. Я буду пользоваться калькой английского слова mock (мок). Суть мока заключается в том, что вместо объекта-зависимости вы используете специальный объект, в котором заменены все методы оригинального класса. Для такого объекта можно сконфигурировать результаты, возвращаемые методами, а также добавить проверки на наличие вызовов методов. PHPUnit содержит встроенный механизм для работы с моками. Одной из его возможностей является создание так называемых частичных моков (partial mocks), когда исходное поведение класса заменяется не полностью, а только для отдельных методов. Такие моки очень удобно использовать, когда вам нужно написать тест, который будет проверять работу конкретного метода и в процессе своей работы вызывать другие методы (которые вы проверять не хотите). Приведу небольшой пример того, где могут быть полезны такие моки. Вот код базового класса, реализующий паттерн «команда»: Реальное поведение команды задаётся в методе execute классов-наследников, а метод run() добавляет общее для всех команд поведение (в данном случае делает код exception safe и логирует ошибки). Если мы хотим написать тест для метода run, мы можем воспользоваться частичными моками, функционал которых предоставляет класс PHPUnit\Framework\MockObject\MockBuilder, доступ к которому предоставляется через вспомогательные методы класса TestCase (в примере это getMockBuilder и createPartialMock): Исходный код, результаты прогона тестов В методе testRunOnSuccess с помощью MockBuilder::setMethods() мы задаём список методов оригинального класса, которые мы заменяем (вызовы которых хотим проверить или результаты которых нужно зафиксировать). Все остальные методы сохраняют свою реализацию из оригинального класса AbstractCommand (и их логику можно тестировать). В testRunOnFailure через метод createPartialMock мы делаем то же самое, но явно. В этом примере всё достаточно просто: мы задаём мокаемые методы и в тесте проверяем их вызов или невызов через expects. В реальном коде бывают и другие случаи, которые требуют переопределения методов:  подготовка или освобождение каких-то ресурсов (например, соединения с базой данных); внешние обращения, которые замедляют тесты и загрязняют окружение (отправка запросов к базе данных, чтение из кеша или запись в него и т. д.); отправка какой-то отладочной информации или статистики. Часто для таких случаев проверок вызова просто нет (поскольку они не всегда нужны и делают тесты хрупкими при изменениях кода). Кроме переопределения существующих методов, MockBulder::setMethods() позволяет добавлять в класс мока новые методы, которых нет в оригинальном классе. Это может быть полезно при использовании в тестируемом коде «магического» метода __call. Возьмём в качестве примера класс \Predis\Client. Он использует метод __call для обработки передаваемых клиенту команд. При этом во внешнем коде это выглядит как вызов конкретного метода и кажется естественным переопределить в создаваемом моке этот вызываемый в коде метод, а не переопределять __call, вдаваясь в детали реализации. Пример: Источник: тест RedisHandlerTest из monolog 2.2.0 Двойственное поведение может приводить к проблемам. Если в моках есть переопределённые методы без expectations, то при их переименовании или удалении тест продолжает проходить (хотя метода уже нет и в его добавлении к моку нет смысла). Небольшая демонстрация. Давайте добавим в код нашего класса команды измерение времени, которое потребовалось для её выполнения: Исходный код В код тестов добавим в мок новые методы, но не будем проверять вызовы через expectations: Исходный код, результаты прогона тестов Если прогнать этот тест в PHPUnit версий 8.5 или 9.5, то он успешно пройдёт без каких-то предупреждений: Конечно, это совсем простой пример,  в который несложно добавить expectations для новых методов. В реальном коде всё может быть сложнее, и мне не раз приходилось натыкаться на несуществующие методы в моках. Ещё сложнее отслеживать подобные проблемы при использовании MockBuilder::setMethodsExcept, который переопределяет все методы класса, кроме заданных. Начало решению этой проблемы «молчаливого» переопределения несуществующих методов было положено в 2019 году в пул-реквесте #3687, который вошёл в релиз PHPUnit 8. В MockBuilder появились два новых метода — onlyMethods() и addMethods() — которые делят ответственность setMethods() на части. onlyMethods() может только заменять методы, существующие в оригинальном классе, а addMethods() — только добавлять новые (которых в оригинальном классе нет). В том же PHPUnit 8 setMethods был помечен устаревшим и появилось предупреждение при передаче несуществующих методов в TestCase::createPartialMock(). Если взять предыдущий пример с некорректным названием метода и использовать createPartialMock вместо вызовов getMockBuilder(...)->setMethods(...), то тест пройдёт, но появится предупреждение о будущем изменении этого поведения: К сожалению, это изменение никак не было отражено в документации — там по по-прежнему была описана только работа setMethods(), а всё остальное было скрыто в недрах кода и GitHub. В PHPUnit 10 проблема setMethods() решена радикально: setMethods и setMethodsExcept окончательно удалены. Это означает, что если вы используете их в своих тестах и хотите перейти на новую версию PHPUnit, то вам нужно убрать все использования этих методов и заменить их на onlyMethods и addMethods. В этой части я дам несколько советов о том, как это можно сделать. Сразу скажу, что для использования этих советов не обязательно ждать выхода PHPUnit 10 и переходить на него. Всё это можно делать в процессе работы с тестами, которые запускаются в PHPUnit 8 или 9. Это кажется совсем очевидным, но во многих случаях этого будет достаточно. Я рекомендую заменить все вхождения и разбираться с падениями. Частично они могут быть вызваны проблемами, описанными выше (и тогда нужно либо удалить метод из мока, либо использовать его актуальное название), а частично — использованием «магии» в мокаемом классе. Если метод, который вы хотите переопределить в моке, работает через «магический» метод __call, то используйте MockBuilder::addMethods(). Если раньше для классов с «магией» вы использовали TestCase::createPartialMock() и это работало, то в PHPUnit 10 это сломается. Теперь createPartialMock умеет заменять только существующие методы мокаемого класса, и нужно заменить использование createPartialMock на getMockBuilder()->addMethods(). В тестах, использующих моки классов из внешних библиотек, всё может быть сложнее из-за того, что там может меняться версия зависимости. Особенно актуально это, если в CI вы используете lowest версии зависимостей вместе со стабильными. Приведу пример из библиотеки PhpAmqpLib. Допустим, вам нужен мок для класса \PhpAmqpLib\Channel\AMQPChannel. В версии 2.4 там был метод __destruct, который отправлял внешний запрос (и поэтому его стоит замокать). В версии 2.5 этот метод был удалён и мокать его уже не нужно. Если в composer.json зависимость прописана подобным образом: "php-amqplib/php-amqplib": "~2.4", то обе версии буду подходить (но моки для них нужны разные) и нужно будет смотреть, какая из них используется. Решать это можно несколькими способами: максимально фиксировать версию библиотеки (например, в приведённом примере можно использовать ~2.4.0 — и тогда разница будет только в patch-версиях); завязываться на версию библиотеки или наличие метода (но это плохой способ, так как для этого нужно внимательно изучать изменения кода всех используемых библиотек, да и очень похоже это на какой-то хак); использовать для классов из внешних библиотек полные моки, а не частичные (но это не всегда возможно). Частичные моки — очень полезный инструмент для написания модульных тестов. К сожалению, разобраться с их изменениями в документации PHPUnit совсем не просто. Надеюсь, что этой статьёй мне удалось как-то это исправить и сделать вашу миграцию на новую версию немного проще. Раздел «Тестовые двойники» документации PHPUnit Pull request "Add onlyMethods + addMethods and soft deprecate setMethods" Pull request "Remove MockBuilder::setMethods() and MockBuilder::setMethodsExcept()" Глава «Unit-тестирование» книги «Архитектура сложных веб-приложений. С примерами на Laravel»