DI, PHPUnit и setUp

    Инверсия зависимостей (Dependency Injection) — весьма приятная вещь, во многом облегчающая жизнь разработчику. Но она же и является причиной появления таких вот конструкторов:

    public function __construct(
        \Psr\Log\LoggerInterface $logger,
        \Zend_Db_Adapter_Pdo_Abstract $dba,
        ISomeService $service,
        ...
    ) {
        $this->_logger = $logger;
        $this->_dba = $dba;
        $this->_service = $service;
        ...
    }

    Использование setUp() в unit-тестах может существенно облегчить жизнь, если нужно несколько раз создать один и тот же набор mock'ов для тестирования различных особенностей реализации разрабатываемого класса.

    Допустим, у нас есть класс с указанным выше конструктором. Для мокирования окружения в отдельном тестовом методе нужно написать что-то такое:

        /* create mocks */
        $mLogger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class)->getMock();
        $mDba = $this->getMockBuilder(\Zend_Db_Adapter_Pdo_Abstract::class)->getMockForAbstractClass();
        $mService = $this->getMockBuilder(\Vendor\Module\ISomeService::class)->disableOriginalConstructor()->getMock();
        ...
        /* setup mocks behaviour */
        ...
        /* */
        $obj = new Demo($mLogger, $mDba, $mService, ...);
        $res = $obj->method($arg1, ...);
        $this->assert...

    Если количество зависимостей в объекте достаточно высоко, а реализуемый им функционал довольно сложен, то unit-тест может содержать изрядное количество блоков с инициализацией mock-объектов, поведение которых затем специализируется в соответствии с проверяемыми требованиями. А если изменилось количество зависимостей в конструкторе, то приходится добавлять новые mock-объекты в каждый тестовый метод и переделывать каждый $obj = new Demo(...);.

    Следуя принципу DRY (Don't Repeat Yourself), следует сосредосточить создание mock'ов в одном месте, а затем уже специализировать их поведение в зависимости от условий тестирования в соответствующем тестовом методе. Это можно сделать при помощи функции setUp. Сначала создаем в PHPUnit'е свойства для самого тестируемого объекта и mock'ов зависимостей:

    private $mLogger;
    private $mDba;
    private $mService;
    private $obj

    а затем прописываем в функции setUp, вызываемую перед каждым тестовым методом, ре-инициализацию mock'ов и объектов:

    private function setUp() {
        $this->mLogger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class)->getMock();
        $this->mDba = $this->getMockBuilder(\Zend_Db_Adapter_Pdo_Abstract::class)->getMockForAbstractClass();
        $this->mService = $this->getMockBuilder(\Vendor\Module\ISomeService::class)->disableOriginalConstructor()->getMock();
        ...
        $this->obj = new Demo($this->mLogger, $this->mDba, $this->mService, ...);
    } 

    после чего специализируем нужное нам поведение mock'ов в соответствующей тестирующей функции:

    public function test_method() {
        /* setup mocks behaviour */
        $this->mLogger->expects...
        ...
        $res = $this->obj->method();
    }

    Отдельное спасибо Fesor за наводку, что лучше использовать setUp(), а не костыль с extract().
    Поделиться публикацией

    Похожие публикации

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

      +2
      Вместо того что бы решить проблему, вы спрятали симптомы проблемы, и тем самым сделали тесты менее полезными. Они должне показывать болячки вашего кода. Именно по этой причине мне так нравятся подходы с PhpSpec и практика TDD.

      вместо extract вы могли бы использовать setup/teardown методы. и тем самым устранить дублирование, готовить тестируемый класс и т.д. а не плодить кастыли. Как-то так это выглядело бы в phpspec. И примерно так же в phpunit.

      class MyObjectSpec extends ObjectSpec {
          /**
           * уже по списко замоканных зависимостей
           * мы можем судить о том, что тестируемый класс
           * явно нарушает принцип единой ответственности
           * так как мы зачем-то и в базу лезем и в логи и еще куда-то...
           * Декорация нас спасла бы и был бы только мок интерфейса сервиса.
           */
          private $logger;
          private $dba;
          private $service;
      
          function let(DBA $dba, MyService $service, Logger $logger) {
              $this->logger = $logger;
              $this->service = $service;
              $this->dba = $dba;
          }
      
          function it_do_something_valuable() {
              $this->service->willReturn('stub');
      
              $this->doSomethingValuable()->shouldReturn('stub');
          }
      
      }
        0
        Как будет выглядеть спецификация, если тестируемый класс выполняет не одну полезную функцию, а, допустим, 10?

        function it_do_something_valuable01() {}
        // ...
        function it_do_something_valuable10() {}

        Меня интересует инициализация/сброс мокируемых зависимостей между определением их поведения в тестирующих функциях (будет весьма нехорошо, если мы проинициализируем моки один раз, а потом будем специализировать поведение одних и тех же моков). Что касается замечательного "принципа единой ответственности", то ему действительно следуют не все, но что делать, если иногда нужно в базу залесть и в логах об этом запись оставить? Для более-менее сложных систем количество объектов, которыми в итоге нужно манипулировать при выполнении некоторой операции может быть весьма велико. Либо мы растем вширь (кол-во зависимостей в конструкторе), либо вглубь (иерархия специализированных манипуляторов с единой ответственностью).
          0
          Как-то так:

          private $mLogger;
          private $mDba;
          private $mService;
          ...
          private $obj;
          
          private function setUp() {
              $this->mLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
              $this->mDba = $this->getMockBuilder('Zend_Db_Adapter_Pdo_Abstract')->getMockForAbstractClass();
              $this->mService = $this->getMockBuilder('Vendor\Module\ISomeService')->disableOriginalConstructor()->getMock();
              ...
              $this->obj = new Demo($this->mLogger, $this->mDba, $this->mService, ...);
          } 

          В этом случае перед каждым тестирующим методом будет происходить реинициализация всех используемых моков и самого тестируемого объекта.

          Спасибо за наводку, Fesor.
            +3
            Как будет выглядеть спецификация, если тестируемый класс выполняет не одну полезную функцию, а, допустим, 10?

            Именно так и будет. По функции/методу на каждый тест кейс. Эти тест кейсы в совокупности и составляют спекицифкацию объекта — что он должен делать. Ну и названия будут описывать именно сам тест кейс, естественно это не будет valuable1...valuable10 а что-то читабельное и осмысляемое с первого прочтения.

            будет весьма нехорошо, если мы проинициализируем моки один раз, а потом будем специализировать поведение одних и тех же моков

            Инициализация моков происходит в setup/teardown. Эти штуки нужны для подготовки прекондишена/посткондишена каждого тест кейса. А вот поведение моков надо задавать в рамках тест кейса, и если оно дублируется — выносить в приватные методы теста к примеру.

            если иногда нужно в базу залесть и в логах об этом запись оставить?

            сервис лазает в базу, сервис-декоратор занимается логированием.

            либо вглубь (иерархия специализированных манипуляторов с единой ответственностью).

            Это не совсем в глубь. Тут вся соль в том, что декораторы мы можем выкидывать. подменять и т.д.
          0
          По поводу ре-инициализации моков между вызовами до меня дошло. Спасибо еще раз.

          сервис лазает в базу, сервис-декоратор занимается логированием.

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

          И я все одно не понял, почему залезть в БД и залогировать это дело в рамках одного класса — есть нехорошо, есть "нарушение принципа единой ответственности"? Ну допустим, мой класс должен находить какие-то данные в базе по несколько причудливому алгоритму, типа "Вернуть А, если не найдено, то Б, если не найдено, то В". А в логах отметить, что именно было возвращено для последующего "разбора полетов" в случае чего. У меня есть логгер и есть адаптер к БД. Почему я не могу использовать оба этих объекта в своем классе?
            +2
            нет, это одна зависимость.

            $service = new MyService(
                new LoggableOtherService(
                     new LoggingService,
                     new OtherService
                )
            );

            когда мы тестируем MyService то нам без нужны знать о том что есть еще какой-то LoggableOtherService, мы мокаем только интерфейс сервиса OtherService и это уносит нас к принципу инверсии зависимостей.

            В вашем же случае наш сервис умеет и логировать, и в базу лазать (через другие сервисы) и т.д. То есть у нас уже область знаний сервиса становится слишком большой. Это нормально в большинстве случаев, но если код надо часто менять (стартап например, коих много) — то декорация позволяет нам изолировать намного больше изменений и намного удобнее управлять системой.
              0
              Тогда я не понимаю, каким образом в приведенном примере MyService залогирует, какой вариант (А, Б или В) он выбрал? Ведь у него нет логгера. Допустим, OtherService лезет в базу и логгирует каждый свой запрос. Но в этом случае логируются все запросы, вне зависимости от того, вызываются ли они из MyService'а или из какого другого места. Более того, иногда возникает необходимость залогировать сообщение с разным уровнем (debug, info, warn, error). И уровень логирования определяется логикой MyService'а. Более того, возможны варианты, когда для уточнения ситуации нужно логировать не сам запрос OtherService, а только часть его (например, ID (не)найденного объекта) в сопоставлении с результатами работы ISomeService (например, запросы к API GitHub'а). Т.е., в MyService реализуется сценарий, когда запрашиваются данные с GitHub'а, анализируются данные в БД, в случае выполнения некоторых условий данные в БД изменяются (или вызывается другой сервис, например IWalletService), если условия не выполняются, то причины логируются. Логгер нужен именно сервису MyService, т.к. логика обработки данных реализована именно в нем.
                0
                Тогда я не понимаю, каким образом в приведенном примере MyService залогирует

                Залогирует LoggableOtherService, а мы тестируем MyService, он не занимается логированием. Если нам надо протестить логирование, мы будем тестировать только LoggableOtherService.

                OtherService лезет в базу и логгирует каждый свой запрос

                class LoggableDB implements DB {
                    private $logger;
                    private $db;
                    public function __construct(DB $db, Logger $logger) {
                         $this->db = $db;
                         $this->logger = $logger;
                    }
                
                    // ...
                
                    public function exec($sql, $params) {
                         $this->logger->log(sprintf('Execute query: "%s", with params: %s', $sql, $params));
                
                         return $this->db->exec($sql, $params);
                    }
                }

                Все ваши кейсы покрываются декорацией.
                  0
                  Залогирует LoggableOtherService, а мы тестируем MyService, он не занимается логированием.

                  Может быть мы под логированием понимаем какие-то разные вещи? Я попытался до вас донести, что в сервисе MyService предположительно реализуется некоторая логика, шаги которой (трейс) было бы хорошо иметь в случае разбора полетов (например, для PSR3-интерфейса есть имплементация, для которой есть обработчик FingersCrossedHandler, позволяющий сбрасывать в лог сообщения всех уровней, если произошла ошибка, и не писать ничего, если ошибки не было). Я полагаю, что в более-менее сложной системе обязательно нужно логировать ключевые моменты в принятии решений (в файл, БД, email'ом — это уже настройки логгера, которые делаются админом приложения). Но вот формирование внятных сообщений — это обязанности разработчика приложения. А какое ж логирование без логгера? Я еще допускаю, что на низком уровне (ближе к БД) можно опустить логирование (генерируется большой объем сообщений), но в сервисах, реализующих бизнес-логику…

                  Из моего примера не следует, кстати, что нужно тестировать имплементацию PSR3-интерфейса, поставляемую в конструктор сервиса, из него следует только, что этот интерфейс должен быть замокирован, чтобы сервис мог в принципе работать (а не вылетать по ошибке "Call to a member function info() on a non-object"). Вот, например, моя типовая операция:

                      public function getRepresentative(Request\GetRepresentative $request) {
                          $this->_logger->info("'Get representative account' operation is called.");
                          ...
                          $this->_logger->info("'Get representative account' operation is completed.");
                          return $result;
                      }

                  Я не проверяю в тестах, что сервис дергает метод info у logger'а, хотя и могу, если вдруг окажется, что данная диагностика критически важна в каких-то ситуациях, когда постфактум пытаешься понять, что произошло на живой системе, в случае проблем.

                  И я абсолютно не вижу причин, по которым PSR3-логгер не может соседствовать в моем сервисе с DBA и/или другими сервисами.
                    +2
                    А какое ж логирование без логгера?

                    Но совсем не обязательно логгер внедрять в зависимости сервиса. Собствено это довольно негибкое решение — сегодня хочется логировать, завтра письма слать, послезавтра какую-то бизнес-логику навешивать. Гораздо гибче передать в сервис какой-нибудь евентдиспетчер, которому сервис будет сообщать "иду по пути А" или "иду по пути Б". А уж слушатели событий будут решать логировать их, ещё что-то делать или игнорировать.
                      –2
                      Но меня не столько интересует, по какому пути мы пошли, как то, почему мы пошли по этому пути. А это можно узнать только в самом сервисе — в нем же реализована бизнес-логика.

                      сегодня хочется логировать, завтра письма слать, послезавтра какую-то бизнес-логику навешивать

                      Тот же Monolog (имплементации PSR3-логгера) позволяет делать это все и многое другое без изменения кода приложения — на уровне конфигурационных файлов. Log4php позволял делать примерно такие же финты (но он из до-PSR3 эпохи). Можно хоть вообще логирование отключить, если оно не нужно, а можно логировать все подряд только от определенного namespace'а (в Log4php, по крайней мере). Можно в файл, можно в БД, можно по email'у, а можно и туда, и туда. И все это — чисто на уровне конфигов уже существующих и широко используемых логгеров — Monolog на packagist'е показывает 24М загрузок. Log4php — 624K.

                      IMHO, логгер вполне широкоприменимый компонент, и он вполне может соседствовать с DBA. ОК, я могу еще согласиться, что DBA не место рядом с другим сервисом в зависимостях, но PSR3-логгер уж точно может идти куда угодно и с кем угодно. Как минимум, в контексте данной статьи.
                        0
                        ок, а если у вас такой случай.
                        при выполнении метода вашего сервиса, вы логируете что-то в 3 местах. и тут так вышло, что 1 из 3 вызовов вам надо писать не только в основное хранилище логов (файлы допустим), а ещё например отправлять письмом.
                        в вашем случаем вам придётся лезть в код вашего сервиса и добавлять этот код, верно?
                          0
                          Если предполагается, что в сервисе могут возникнуть различные варианты обработки лог-сообщений (помимо стандартного уровня — debug, info, ...), то нужно маркировать подобные сообщения на этапе девелопмента (см. второй параметр $context в LoggerInterface). После чего на уровне конфигурирования можно перенаправлять ваши сообщения куда хотите.

                          Если вы сначала написали код, а потом решили, что он должен работать по-другому, то да — код нужно будет менять.
                            0
                            нет, дело как раз в добавлении дополнительной логики логирования. или к примеру для конкретного лог-сообщения вам нужно поменять уровень с debug на warning.

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

                            а решение, которое вам предложил VolCh основано на вот таком принципе/поведении http://symfony.com/doc/current/components/event_dispatcher/introduction.html

                            это гораздо более гибкий вариант поведения
                              0
                              для конкретного лог-сообщения вам нужно поменять уровень с debug на warning.

                              это гораздо более гибкий вариант поведения

                              Т.е., в более гибком варианте поведения мне нужно будет изменить код слушателя, я правильно понял?
                                0
                                ну гибкость заключается в том, что вы можете добавлять/удалять слушателей без изменения кода с бизнес-логикой.

                                а по поводу изменения уровня логирования, то преимущество, что вы изменяете слушателя, который занимается логированием, а не класс с бизнес-логикой.

                                да и по правде говоря, бизнес-логика в сервисе, на мой взгляд, находиться на другом уровне абстракции нежели логирование.

                                реальный кейс — так сложилось, что на одном из проектов у нас был свой реализованный PSR-3 логгер. он был очень простой, писал в syslog и никак в общем-то не конфигурировался.

                                и тут появилась необходимость логгировать критические ошибки для некоторых модулей напрямую в Slack канал.
                                всё, что мы сделали — добавили в необходимые слушатели вызовы PSR-3 совместимого логгера в Slack.

                                как вы бы решали конкретно такую проблему, если бы вызывали напрямую логгер в сервисе с бизнес-логикой?
                                  0
                                  Для начала я бы использовал готовый PSR3-фреймворк — Monolog. Затем, убедившись, что он не конфигурируется через внешние файлы, я бы обернул его в monolog-cascade. После чего я имел бы возможность через конфигурационный файл влиять на все логи приложения с возможностью их вывода в "никуда"/файл/базу/syslog/email/… С использованием процессора IntrospectionProcessor (подключаемого через конфиг, когда мне нужно) я имел бы возможность расширять сообщения до такого формата:
                                  [YYYY-MM-DD HH:MM:SS] main.DEBUG: MESSAGE_HERE. {"file":"/.../Main_Test.php","line":75,"class":"...\\Main_IntegrationTest","function":"test_main"}

                                  Чего мне уже бы хватило для нахождения в лог-файле (я в основном использую их, но можно и в БД лить) сообщения с конкретной строки конкретного метода конкретного класса (вне зависимости от его уровня логирования).

                                  Если же мне и этого мало, а мне нужно по конкретному сообщению создавать, например, таск support'у в JIRA, то я могу написать свой handler/processor/formatter и подключить его через конфиг-файл без изменения кода сервиса:

                                  public function __construct(
                                      \Psr\Log\LoggerInterface $logger,
                                      ...
                                  ) {
                                      $this->_logger = $logger;
                                      ...
                                  }
                                  
                                  public function operation($request) {
                                      $this->_logger->info("Operation is called.");
                                      ...
                                  }

                                  Если нужна еще большая гибкость, то да — это решение не подходит, и нужно применять что-то другое. Возможно даже EventDispatcher.
                          +1
                          Тот же Monolog (имплементации PSR3-логгера) позволяет делать это все и многое другое без изменения кода приложения — на уровне конфигурационных файлов.

                          что именно? Письма слать он может?

                          Просто ваш код должен быть разделен с соблюдением SOLID, обладать низкой связанностью и высоким зацеплением. И если так — то все эти задачи с логированием можно выносить как декораторы к сервисам и таким образом достигать мега гибкой архитектуры, где любую реализацию можно выкинуть или заменить.

                          А если вам гибкость не так важна (fixed-price аутсорс) — то тут плевать вообще как делать. Но коль уж делать ОО то делать его надо правильно а не как придется. Увы сегодня большинство считает что достаточно просто юзать классы что бы было ОО.
                            0
                            что именно? Письма слать он может?

                            Да.

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

                            В Java этого добра поболее — java.util.logging, log4j, slfj4, в PHP я использовал только log4php и собственно Monolog (PSR3 имплементацию). Я не вижу смысла проектировать логгеры — они уже есть. Остается их только использовать там, где нужно. А в сложных системах, да еще и в случае "гибкой архитектуры", их нужно использовать чуть менее, чем везде (слегка утрирую, но для сервисов, реализующих бизнес-логику — везде).

                            Если вы же и поддерживаете те приложения, архитектором которых вы являетесь, у вас не может быть другого мнения на этот счет.
                              0
                              Вы про алертику, а я про отправку сообщений. То есть у нас есть сервис логгера, сервис мэйлер, декоратор для логгера с зависимостью от мэйлера который в случае ошибок отправляет нам сообщения. Три сервиса (один из них декорирует другой).

                              То о чем вы толкуеет — это просто логгер (PSR-3). Я же говорю о том что действия наших сервисом можно логать и извне (что правильнее в большинстве случаев), создавай декораторы. Как правило подобное надо только для отладки какой-то, а так нам хватит обычного обработчика исключений сверху системы.

                              То есть вместо того что бы в сервис A инджектить логгер, я сделаю декоратор для сервиса ALoggable и туда впихну логгер, что бы оставить код сервиса A без изменений, а логирование вынести отдельно. В этом случае мне не нужно будет править тесты тех сервисов, которые зависят от A, или же сами тесты A.
                                0
                                Чем интерфейс "логгер-декоратора" отличается от PSR3?

                                interface LoggerInterface
                                {
                                    public function emergency($message, array $context = array());
                                    public function alert($message, array $context = array());
                                    public function critical($message, array $context = array());
                                    public function error($message, array $context = array());
                                    public function warning($message, array $context = array());
                                    public function notice($message, array $context = array());
                                    public function info($message, array $context = array());
                                    public function debug($message, array $context = array());
                                    public function log($level, $message, array $context = array());
                                }

                                Можете привести для примера 2-3 публичных метода "логгер-декоратора"?
                                  0
                                  Чем интерфейс «логгер-декоратора» отличается от PSR3?

                                  Для начала определимся что такое "логгер-декоратор". Рассмотрим два случая.

                                  1) отправка сообщений на email о том что что-то сломалось.

                                  class AlertLoggerDecorator implements LoggerInterface {
                                      private $logger;
                                      private $notifier;
                                      private $levels;
                                  
                                      public function __construct(LoggerInterface $logger, Notifier $notifier, array $levels = []) {
                                          $this->logger = $logger;
                                          $this->notifier = $notifier;
                                          $this->levels = $levels;
                                      }
                                  
                                      public function emergency($message, array $context = array()) {
                                           $this->log(LogLevel:: EMERGENCY, $message, $context);
                                      }
                                  
                                      // ...
                                  
                                      public function log($level, $message, array $context = array()) {
                                          $this->logger->log($level, $message, $context);
                                          if (in_array($level, $this->levels)) {
                                                $this->notifier->notify($message); // либо это вынести в отдельный приватный метод
                                          }
                                      }
                                  }

                                  а если мы хотим логировать какие-то действия сериса

                                  class MyServiceLoggable implements MyServiceInterface {
                                      private $service;
                                      private $logger;
                                  
                                      public function __construct(MyServiceInterface $service, LoggerInterface $logger) {
                                           $this->service = $service;
                                           $this->logger = $logger;
                                      }
                                  
                                      public function doSomething($arg1, $arg2) {
                                          $this->logger->debug('something is about to happen: {arg1}, {arg2}', compact('arg1', 'arg2'));
                                          $result = $this->service->doSomething($arg1, $arg2);
                                          $this->logger->debug('result of something: {result}', compact('result'));
                                      }
                                  }

                                  что-то в этом духе. Очень редко нам надо дебажить что-то внутри одного метода "логами", и я считаю это плохой практикой. В этом 1% случаев можно заинджектить логгер прямо внутрь интересующего нас сервиса, но лучше уж тестами покрыть.
                                    0
                                    Спасибо за код. Я сравниваю свой код из статьи:

                                    public function __construct(
                                        \Psr\Log\LoggerInterface $logger,
                                        \Zend_Db_Adapter_Pdo_Abstract $dba,
                                        ISomeService $service,
                                        ...
                                    ) {...}

                                    и ваш пример:

                                    public function __construct(MyServiceInterface $service, LoggerInterface $logger) {
                                         $this->service = $service;
                                         $this->logger = $logger;
                                    } {...}

                                    и, честно сказать, не нахожу особой разницы. С точки зрения темы, освещаемой в статье, ее нет вообще.

                                    Я хотел для примера воткнуть хоть какие-то более-менее правдоподобные зависимости в конструктор, чтобы не писать "ISomeService1", ..., "ISomeService4", а получилось, что я нарушил "принцип единственной ответственности". В результате дискуссия ушла в сторону правомерности использования логирования для трассировки выполнения бизнес-операций.

                                    Спасибо, что осветили свою точку зрения на этот вопрос (про логирование) — мне, по крайне мере, было очень интересно.
                                      +1
                                      и, честно сказать, не нахожу особой разницы

                                      Разница существенная. У MyServiceLoggable и $service один интерфейс. По сути экземпляр MyServiceLoggable подменяет $service, не меняя контракта для клиентов, но производя дополнительный сайд-эффект — логирование.
                                        +1

                                        Возможно, я плохо выражаю свои мысли. Если вы посмотрите на мои примеры, то я нигде не указывал, что конструктор соответствует классу, имплементирующему какой-то определенный интерфейс. Интерфейс, который имплементируется, вообще остается за рамками вопроса, рассматриваемого в статье. А если не придумывать лишнего, то как я и сказал, разница между конструктором с 3-мя параметрами, и конструктором с 2-мя параметрами — несущественная.

                                          0
                                          Интерфейс, который имплементируется, вообще остается за рамками вопроса, рассматриваемого в статье

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

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

                                              +1

                                              А чего вы к декораторам привязались? Статья про тестирование, а не про полиморфизм. Тестирование без конструирования невозможно, а без полиморфизма — вполне. Еще раз, с точки зрения создания объекта очень сильно незначительно важно, сколько у него параметров в конструкторе — 2 или 3.


                                              Весь остальной наш флейм к сути вопроса, освещаемого в статье, имеет весьма малое отношение. Даже еще меньшее, чем имеет конструктор класса и его параметры к имплементируемым классом интерфейсам.

                                                –1

                                                декораторы — очень хороши в плане тестирования. Расширять ими функционал одно удовольствие. Они так же хороши для снижения связанности системы. И все обсуждение строго в рамках материала статьи.

                                                  0

                                                  Какой статьи? "DI, PHPUnit и setUp"? Там есть хоть слово про декораторы или интерфейсы? Каким образом вы связали декораторы и изложенное в статье, по каким ключевым словам? По слову "тестирование"?

                                                    0

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


                                                    • описанное в статьи решает проблему инициализации большого количества зависимостей (аж 3 штуки, логи, база данных и еще че-то там)
                                                    • наши тесты этим намекали что у нас нарушен принцип единой ответственности и повысилась связанность системы
                                                    • декораторы — простой способ снизить количество зависимостей и уменьшить связанность системы, повышая при этом зацепление.
                                                      +1

                                                      На это я уже ответил


                                                      Я хотел для примера воткнуть хоть какие-то более-менее правдоподобные зависимости в конструктор, чтобы не писать "ISomeService1", ..., "ISomeService4", а получилось, что я нарушил "принцип единственной ответственности".

                                                      Если заменить в моей статье


                                                      \Psr\Log\LoggerInterface $logger,
                                                      \Zend_Db_Adapter_Pdo_Abstract $dba,
                                                      ISomeService $service,

                                                      на


                                                      ISomeService1 $service1,
                                                      ISomeService2 $service2,
                                                      ISomeService3 $service3,

                                                      то все, что касается декорирования, начиная с вашего первого коммента просто повиснет в воздухе, а суть моей статьи не изменится не то, что хоть как-нибудь — она вообще не изменится.


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

                +2
                Советую использовать ::class для получения имени класса в моках, чтобы IDE могла индексировать использования класса:

                $mLogger = $this->getMockBuilder(LoggerInterface::class)->getMock();
                $mDba = $this->getMockBuilder(Zend_Db_Adapter_Pdo_Abstract::class)->getMockForAbstractClass();
                $mService = $this->getMockBuilder(ISomeService::class)->disableOriginalConstructor()->getMock();
                  0
                  +1 это ещё и короче с namespase-ами. И зависимости лучше видно
                    0
                    Спасибо за совет. Внес правки в статью.
                    +2
                    Если количество зависимостей в объекте достаточно высоко, а реализуемый им функционал довольно сложен

                    Если зависимостей слишком много то их нужно выделять в отдельные сервисы. Я придерживаюсь правила не более 4 зависимостей на класс.

                    Если логика слишком сложная то её нужно разносить на отдельные сервисы. Где-то можно сгруппировать действия, где-то бросить событие, где-то ввести уровень абстракции. Хорошую планку задает SensioLabsInsight — метод должен быть не длиннее 50 строк.
                      –2
                      Вот, например, вполне реальный пример, в котором придерживаются принципа "<50 per method". А это — его конструктор.

                          public function __construct(
                              Vat $customerVat,
                              HelperAddress $customerAddress,
                              Registry $coreRegistry,
                              GroupManagementInterface $groupManagement,
                              ScopeConfigInterface $scopeConfig,
                              ManagerInterface $messageManager,
                              Escaper $escaper,
                              AppState $appState
                          ) {
                              $this->_customerVat = $customerVat;
                              $this->_customerAddress = $customerAddress;
                              $this->_coreRegistry = $coreRegistry;
                              $this->_groupManagement = $groupManagement;
                              $this->scopeConfig = $scopeConfig;
                              $this->messageManager = $messageManager;
                              $this->escaper = $escaper;
                              $this->appState = $appState;
                          }

                      Придерживаясь правила "не более 4-х", мы можем выделить зависимости в отдельные сервисы, увеличив кол-во классов, и превратив 300 строк кода в даже не знаю сколько. Можете попробовать отрефакторить код примера, если у вас есть желание и время, и сравнить его с первоначальным — улучшиться ли читаемость и управляемость в результате.

                      В данном случае при рефакторинге придется наращивать кол-во узкоспециализированных классов, что я и называют "ростом вглубь", в отличие от "роста вширь", когда в классе используется "более 4-х" зависимостей с более широкой специализацией. Плюс "широкой специализации" — порог вхождения в проект ниже, чем порог вхождения в проект с "узкой специализацией". Плюс "узкой специализации" — позволяет строить в конечном итоге более сложные проекты, чем "широкая" (правда в этом случае нужно очень хорошо представлять себе предметную область, а "широкая" более снисходительна к экспериментам).

                      За ориентир "4 зависимости" — спасибо :)
                        +1
                        улучшиться ли читаемость и управляемость в результате.

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

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

                        Допустим в вашем примере я не могу придумать кейс, при котором нам нужен escaper рядом с vat и зачем оно используется. Да и вообще customerVat/customerAddress больше похоже на VO или какое-либо состояние, а состояние мы не должны инджектить в конструкторы. Мы должны получать его из сервисов.

                        Словом… такой код в моем случае ревью бы не прошел.
                          0
                          Это не мой код. Это просто один из классов Magento 2 — а их там много. Ссылка на репозиторий есть, можете сами убедиться, что это не исключительный случай. Я не говорю, что это хорошо или плохо. Я отмечаю, что это есть. Если в системе есть 100 классов, то их можно свести к 4-м зависимостям (хотя человек способен достаточно комфортно оперировать объектами в количестве 7 плюс/минус 2, т.е., для разработчика это скорее в плюс, чем в минус — итого 9). Но и тут придется увеличить глубину иерархии "сервис вызывает сервис, который вызывает сервис, вызывающий сервис". Что тоже в конце-концов, при увеличении кол-ва реализованных функций и глубины залегания нужных сервисов, приведет всех в уныние. Вообщем, я тоже за здравый смысл. Вот только он у всех здрав по разному :)
                      0
                      честно говоря, статья совсем уж пустая. То, что можно сохранить моки в приватном свойстве догадаться не составляет труда.

                      Вы бы дополнили статью информацией о том, как правильно работать с моками, как переиспользовать например поведение, сложные случаи и тд
                        0
                        Для вас не составило. Для меня — да. Сработала инерция мышления. Изначально я вообще использовал функцию extract() и заголовок был "DI, PHPUnit и extract", а не как сейчас. Коллега Fesor навел меня на setUp(). Да, я смотрел тесты для класса AfterAddressSaveObserver.php и видел, что моки сохраняются в приватных свойствах. Да, я не заметил, что именно так там и было сделано — через приватные свойства и setUp. Да, я загуглил запрос "DI phpunit", перед тем, как написать хабр (с костылем extract, кстати).

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

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

                      Самое читаемое