Magento 2: Когда использовать диспетчер объектов в модульных тестах?


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

Винай сказал мне, что, как правило, плохая практика использовать диспетчер объектов в модульных тестах:

В модульных тестах вообще не используйте ObjectManager. Просто создайте экземпляр своего класса с помощью new, так как все зависимости в любом случае являются тестовыми двойниками. Меньше магии облегчает отладку. Особенно нравится в вашем случае, когда существует только одна зависимость.

Сейчас... Я сделал то, что он предложил, и я изменил свое построение класса на основе объектного менеджера на ванильное new-утверждение, и оно сработало:

Итак, вместо этого:

$this->sourceModel = (new ObjectManager($this))
    ->getObject(
        \Custom\Shipping\Model\Config\Source\ProductTypes::class,
        [
            'Config' => $this->configHelperMock
        ]
    );

Я сделал это:

$this->sourceModel = new \Custom\Shipping\Model\Config\Source\ProductTypes($this->configHelperMock);

Но у меня также есть еще один тест, который должен был заглушить помощника. Это я сделал так:

$this->configHelper = (new ObjectManager($this))
    ->getObject(
        \Custom\Shipping\Helper\Config::class,
        [
            'scopeConfig' => $this->scopeConfigMock
        ]
    );

Теперь, если вы посмотрите на класс AbstractHelper Magentos, вы заметите, что у него есть один аргумент конструктора: Magento\Framework\App\Helper\Context. Так что я понял, что менеджер объектов делает с указанным выше вызовом конструктора, это устанавливает scopeConfig-параметр моего Context-объекта в мой издевательский объект. Я надеюсь, что это предположение верно.

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

Я действительно пытаюсь понять, как правильно писать модульные тесты для Magento, потому что на данный момент мне кажется, что:

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

Может ли кто-нибудь пролить свет на это? В документации Magento 2 отсутствует более подробное объяснение того, как писать правильные модульные тесты.

Author: Community, 2016-10-12

2 answers

Короткий ответ: да, я бы высмеял весь класс Context.

$mockScopeConfig = $this->getMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$mockContext = $this->getMock(\Magento\Framework\App\Helper\Context::class, [], [], '', false);
$mockContext->method('getScopeContext')->willReturn($mockScopeConfig);

$classUnderTest = new \Custom\Shipping\Helper\Config($mockContext);

Немного предыстории

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

Основными родительскими классами, от которых наследуется мой тестируемый класс, являются особенно проблематично, потому что я хочу только протестировать код, который я пишу (в модульных тестах). Я не хочу тестировать основной код.

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

Если бы я писал конструктор вашего помощника так, как мне хотелось бы, это выглядело бы примерно так:

public function __construct(HelperContext $context, ScopeConfigInterface $scopeConfig)
{
    parent::__construct($context);
    $this->myScopeConfig = $scopeConfig;
}

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

К сожалению, однако Magento заставляет нас связать наш класс с классом Context из-за досадной проверки во время bin/magento setup:di:compile.

Из-за этого я бы написал конструктор так:

public function __construct(HelperContext $context)
{
    parent::__construct($context);
    $this->myScopeConfig = $context->getScopeConfig();
}

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

Из-за наследия Magento 1, однако, не весь код Magento 2 следует этому принципу.
Поэтому иногда мне приходится имитировать части контекстного класса или родительские зависимости, даже если они не нужны моему коду. Я делаю это вручную, хотя и не полагаюсь на помощника модульного теста ObjectManager.
(Один примером требуемого класса являются сущности, расширяющиеся от \Magento\Framework\Model\AbstractModel.)

Заключение

Наконец, я хотел бы спросить вас, почему вы расширяете AbstractHelper в первую очередь?
Только потому, что основные модули делают это так?
Дает ли это какую-либо реальную выгоду?
Если вашему классу требуется только конфигурация области действия, почему бы не иметь БЕЗ родительского класса и просто напрямую зависеть от ScopeConfigInterface?

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

 11
Author: Vinai, 2018-01-26 14:50:01

Чтобы ответить на ваш вопрос, я опишу, почему вспомогательный класс ObjectManager был разработан командой Magento: Когда началась разработка M2, все классы использовали методы Mage::*, и мы начали увеличивать охват с помощью модульного тестирования. Но когда мы добавляем новую инъекцию конструктора, многие тесты запускаются неудачно. Итак, мы решили ввести вспомогательный класс, чтобы избежать ненужной работы.

Но теперь зависимости конструктора стабилизированы, поэтому помощник в большинстве случаев не нужен

 4
Author: KAndy, 2016-10-12 08:52:41