Magento 2 – When to Use Object Manager in Unit Tests

magento2object-managerphpunitunit tests

So I recently stumbled upon a problem while writing a unit test in Magento 2. You can read more about the problem in question here.

Vinai told me it is generally bad practice to use the object manager in unit tests:

In Unit tests, don't use the ObjectManager at all. Just instantiate your class with new, since all dependencies are test doubles anyway.
Less magic makes for easier debugging. Especially like in your case where there is only a single dependency.

Now… I did what he suggested, and I changed my Object-Manager based constructing of the class to a vanilla new-statement and it worked:

So instead of this:

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

I did this:

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

But I also have another test that needed to stub a helper. This I did like this:

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

Now if you look at Magentos' AbstractHelper-class, you'll note that it has one constructor argument: Magento\Framework\App\Helper\Context. So I figured what the Object Manager does with above constructor call, is it sets the scopeConfig-parameter of my Context-object to my mocked object. I hope this assumption is correct.

But… to get back to Vinais' statement to not use the Object Manager, how would you do this without the Object Manager? Mock the entire Context object? Or is the Object Manager the correct strategy to use in this particular scenario?

I'm really trying to understand what's the correct way to write unit tests for Magento, because for now it seems to me that:

  • You need to use the Object Manager when you want to mock the content of the context-object
  • You don't need to use the Object Manager in all other scenarios.

Can anyone shine a light on this? The Magento 2 documentation is lacking of a more in-depth explanation on how to write proper unit tests.

Best Answer

The short answer is, yes, I would mock the entire Context class.

$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);

A bit of background

The base of my reasoning here is that I'm assuming it is good for classes to know as little as possible about other classes, including their parents.
I'm asserting this is correct because less knowledge about other classes means less broken code due to changes in other classes.

Core parent classes that my class under test inherits from are particular problematic because I only want to test code I write (in unit tests). I don't want to test core code.

Because of that I try to keep my code as decoupled as possible from core parent classes.
This means I do not use any dependencies from parent classes, for example, by accessing their protected properties.

If I would be writing the constructor of your helper the way I would like, it would look something like this:

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

You see, this class is only marginally coupled to the parent dependency. I don't want my class to know about the parent or its dependencies.

Unfortunately however Magento forces us to couple our class to the Context class due to an annoying check during bin/magento setup:di:compile (update: this check has been removed since 2.2.0).

Because of this I would write the constructor like this:

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

If the parent class follows the design principles tell-don't-ask, then my class will not need to call any method of the parent class. Instead, all my class will have to do is implement an abstract method which will be called by the parent at the appropriate time (in pattern lingo this is the template method pattern).

Due to the legacy of Magento 1 however not all Magento 2 code follows this principle.
So sometimes I have to mock parts of a context class or parent dependencies, even if my code doesn't need them. I do so manually though and don't rely on the unit test ObjectManager helper.
(One example of a class that is required is entities extending from \Magento\Framework\Model\AbstractModel.)

Conclusion

Finally, I would like to ask you why you are extending the AbstractHelper in the first place?
Just because core modules do it like that?
Does it provide any real benefit?
If your class only requires the scope config, why not have NO parent class and just depend on the ScopeConfigInterface directly?

That way you avoid a lot of test doubles and potential breakage when core code changes, plus you have less unnecessary overhead during runtime because all the other unneeded dependencies don't have to be instantiated.