Как стать автором
Обновить

Комментарии 16

Огромное спасибо, как для новичка в продакшене очень интересный материал

Имеется большой проект со старой кодовой базой, написанной на древнем фреймворке, в котором довольно активно используются статические методы и свойства классов. Какие хитрости можно применить для тестирования такого?

Если покрытия никакого нет совсем, то я бы начал в вашем случае с е2е тестов, чтоб зафиксировать поведение системы.

Обычно со статическими методами нет никаких проблем с тестированием, пока это чистые функции (нет side effects) и нет использования других статических классов и методов.
Проблема зависимости статического метода от других статических классов разбивается на подпроблемы. Первая подпроблема: возможность управлять поведением зависимого класса. Например, это некий класс Config, который в рамках работы приложения может вообще быть readonly. Но в рамках теста мы можем подсунуть другую (более управляемую) реализацию сманипулировав на includ'ах. Понятно, что потом уже класс Config уже потестить не получится. Поэтому чаще для таких классов добавляется возможность внешнего управления. Вторая подпроблема: созависимый класс все-таки очень хочется подменить mock'ом. Тут либо DI, либо runkit. Но лучше DI (для кодовой базы и развития лучше), хоть и более трудней добавлять.

Ну тут в основном проблема в инициализации статических свойств классов. По-хорошему для чистоты эксперимента перед каждым тестом эти свойства нужно "очищать". Проблема в огромном количестве таких мест.

У нас, собственно, тоже хватало таких моментов. Вводили базовый класс для TestCase, в нем setUp и по-тихоньку добавляли туда ClassA::setup/setInstance/init, а в tearDown уже ClassB::reset(), ClassC::reset() и т.п. С одной стороны, мест много. С другой стороны, начинаем с теста на какой-то конкретный метод и он уже имеет ограниченное количество зависимостей, стараемся "добить" и максимально изолировать этот метод. А далее, только упорство :)

Потихоньку оборачивать их в инжектящиеся сервисы?

Codeception все-таки не для unit-тестирования. То есть делать его, конечно, можно, но мне не очень понятно зачем. Попытки использования Codeception для написания acceptance-тестов (в виде BDD) у нас были и некоторое количество тестов осталось до сих пор. Очень сомнительно в плане долгосрочной поддержки, поэтому развивать не стали. Большая часть e2e на модифицированный PHPUnit/Selenium/WebDriver и сейчас делаем довольно масштабный переход на Cypress.


P.S. Последняя картинка в более высоком разрешении — https://habrastorage.org/webt/kz/ek/ay/kzekaym4io39v3hwigwxkref_n0.png

Понятно.

В компаниях, где я работал, обертка над тестами в виде Codeception была по умолчанию — чтобы в случае необходимости добавить приемочные тесты — их можно было легко добавить без дополнительных настроек.

Вставлю свои 5 копеек — работал на проекте, который предоставлял исключительно json-апи для фронта на ангуляре. И все апи покрывали функциональными тестами на codeception. И это было невероятно удобно, также с апи очень легко работал подход TDD

Как вы пишете expects на моках? На моем проекте мы выносим их в переиспользуемые хелпер методы вроде


protected function expectFindOneBy(MockObject $repository, array $criteria, ?object $result): void
    {
        $repository->expects($this->once())
            ->method('findOneBy')
            ->with($criteria)
            ->willReturn($result);
    }

Пользуетесь ли вы withConsecutive или как-то избегаете их? МБ вы используете другую библиотеку моков, вроде phpspec/prophecy

Как вы пишете expects на моках? На моем проекте мы выносим их в переиспользуемые хелпер методы

В зависимости от ситуации. Хелперы есть, прямое использование в рамках теста — тоже. Эволюционно мы пришли к мысли стараться в тестах делать как можно меньше абстракций и дополнительных слоев. В идеале — идея теста четко видна из кода тестового метода. Шанс, что мы наймем разработчика, знакомого с PHPUnit, очень высок, а вот шанс, что новый разработчик будет знаком с нашими доморощенными наворотами и абстракциями — равен нулю. Некоторое дублирование в рамках кода тестов — это вполне ок, если это улучшает их читабельность. В целом, стараемся это применять ко всем тестовым фреймворкам, которые используем.


Пользуетесь ли вы withConsecutive

Да. Не конкретно к этому методу, но в целом к мокам отошение следующее: чем их меньше, тем лучше :)


МБ вы используете другую библиотеку моков, вроде phpspec/prophecy

Нет, не используем.

пришли к мысли стараться в тестах делать как можно меньше абстракций и дополнительных слоев.

Интересно, есть ли плагин для PHPStorm для автодополнения имени метода для ->method() в PHPunit. МБ и хелперы были бы тогда менее актуальны.


К мокам отошение следующее: чем их меньше, тем лучше

А как уменьшить их количество? Мокаются ведь все инжектящиеся сервисы?


Про withConsecutive… Лично я просто очень его не люблю. :) Тестовый код с ним становится очень хрупким, плохо расширяется и начинает сильно отличаться от непосредственно боевого кода. Может, конечно, я к нему слишком предвзят, но даже написал обертку над MockObject интерфейсом, которая собирает все вызовы expects на моке, а потом под капотом делает withConsecutive. Так код с множественными вызовами мока становится аналогичным коду с единственным вызовом.

А как уменьшить их количество?

Не использовать для тестирования классов с большим количеством зависимостей юнит-тесты. Заменять их e-2-e или интеграционным тестами.
По опыту чем больше зависимостей ты мокаешь, тем хрупче твой тест, и любое изменение в классе, даже без изменения внешнего контракта, перерастает в борьбу с тестами.

На мой взгляд, если и пользоваться моками в юнит-тестах, то только в качестве заглушек, а не шпионов. То есть без вызова expect. Аргументация: нарушения инкапсуляции.

Т.е. писать юнит тесты только для чистых функций? Вряд ли большая часть кода проекта не будет иметь внешние зависимости.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.