Vinai Kopp

Magento Expert, Developer & Trainer

  • 08. The DI Arguments Config Kata

    May 5, 2016

    Mage2Katas

    Dependency Injection is at the technological heart of Magento 2. It is used to wire together the application.
    Using the di.xml configuration files most of the systems behavior can be changed.

    Given it is such a critical part of the application, I like being able to know what I am configuring is correct.

    As a scenario for this kata we will be configuring the objects used to read, validate and access data from a custom XML configuration file.

    In Magento 2 each aspect of reading, merging, transforming and accessing configuration data is handled by separate classes, each one with their own interface.
    We can use custom implementations, or use generic classes configured via di.xml. For this kata we will be doing the latter.

    To keep this episode short and focused, this kata will only be about checking the DI configuration.
    We will create the full solution to use a custom config file in another Mage2Kata episode.

    The custom configuration file that I’m using is called unit_conversion.xml, but it doesn’t matter yet for this episode.
    We will get back to this in future.

    The module name will be Mage2Kata_DiConfig.

    Since we are checking configuration, we will be using integration tests.
    The first step of this kata is to create the folders Test/Integration/ and the file DiConfigConfigurationTest.php within the module.
    The test class name is a little weird, but I like to include the module name in such configuration tests, so that is how I came up with it.

    Again, I like to get ready with a testNothing() test to check I’ve got my environment set up correctly.

    <?php
     
    namespace Mage2Kata\DiConfig;
     
    class DiConfigConfigurationTest extends \PHPUnit_Framework_TestCase
    {
        public function testNothing()
        {
            $this->fail('here we go');
        }
    }

    All okay? Good.
    Step one in this kata is to check the generic \Magento\Framework\Config\Data class is aliased to a virtual type for our custom configuration file.

    One way to check this is by using the Object Manager configuration.
    In order to get the fully loaded instance, we have to ask the Object Manager for the interface Magento\Framework\ObjectManager\ConfigInterface (using the implementation directly would only give us an empty new instance).

    <?php
     
    namespace Mage2Kata\DiConfig;
     
    use Magento\Framework\ObjectManager\ConfigInterface as ObjectManagerConfig;
    use Magento\TestFramework\ObjectManager;
     
    class DiConfigConfigurationTest extends \PHPUnit_Framework_TestCase
    {
        public function testUnitMapConfigDataVirtualType()
        {
            /** @var ObjectManagerConfig $diConfig */
            $diConfig = ObjectManager::getInstance()->get(ObjectManagerConfig::class);
            $this->assertSame(
                \Magento\Framework\Config\Data::class,
                $diConfig->getInstanceType(Model\Config\Data\Virtual::class)
            );
        }
    }

    Note that the class \Mage2Kata\DiConfig\Model\Config\Data\Virtual does not exist. (Somehow we end up mostly dealing with non-existent classes in these katas, don’t we?).
    We want the class to be an alias for \Magento\Framework\Config\Data, but will be adding some specific DI argument configuration.

    Executing this first test fails.

    Failed asserting that two strings are identical.
    Expected :Magento\Framework\Config\Data
    Actual   :Mage2Kata\DiConfig\Model\Config\Data\Virtual

    To make it pass, lets add the etc/di.xml file with the following configuration:

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="Mage2Kata\DiConfig\Model\Config\Data\Virtual" type="Magento\Framework\Config\Data">
        </virtualType>
    </config>

    Note: If you have the TESTS_CLEANUP constant set to disabled in the phpunit configuration, clear the dev/tests/integration/tmp/sandbox-* directory.
    Do this after every configuration XML change.

    Lets run the test again. All is green. Now before we continue, lets refactor to increase the expressiveness of our test code a little.

    Since the DI configuration will be used in subsequent tests, too, lets extract it into a query method. Also lets extract the test into a simple custom one-line assertion method.

    class DiConfigConfigurationTest extends \PHPUnit_Framework_TestCase
    {
        /**
         * @return ObjectManagerConfig
         */
        private function getDiConfig()
        {
            return ObjectManager::getInstance()->get(ObjectManagerConfig::class);
        }
     
        /**
         * @param string $expectedType
         * @param string $type
         */
        private function assertVirtualType($expectedType, $type)
        {
            $this->assertSame($expectedType, $this->getDiConfig()->getInstanceType($type));
        }
     
        public function testUnitMapConfigDataVirtualType()
        {
            $type = Model\Config\Data\Virtual::class;
            $this->assertVirtualType(\Magento\Framework\Config\Data::class, $type);
        }
    }

    All tests still green? Okay.
    Time for step two.

    Lets have a look at the \Magento\Framework\Config\Data constructor.

    /**
     * @param ReaderInterface $reader
     * @param CacheInterface $cache
     * @param string $cacheId
     */
    public function __construct(ReaderInterface $reader, CacheInterface $cache, $cacheId)
    {
        $this->reader = $reader;
        $this->cache = $cache;
        $this->cacheId = $cacheId;
        $this->initData();
    }

    One of the constructor arguments the class expects is called $cacheId. According to the phpdoc annotation that should be a string.
    For the kata, lets say we want the cache ID to be mage2kata_config_unitconversion_map.

    We extend the first test as follows:

    public function testUnitMapConfigDataVirtualType()
    {
        $type = Model\Config\Data\Virtual::class;
        $this->assertVirtualType(\Magento\Framework\Config\Data::class, $type);
     
        $argumentName = 'cacheId';
        $arguments = $this->getDiConfig()->getArguments($type);
        if (! isset($arguments[$argumentName])) {
            $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
        }
    }

    Note: Why use $this->fail() instead of for example $this->assertArrayHasKey()? We will extract this part (and a little bit more) of the method into another custom assertion method. By only using one assert* method, the assertion count of PHPUnit will still be correct.

    This is enough to have a failing test again.
    Here is the configuration to make it pass.

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="Mage2Kata\DiConfig\Model\Config\Data\Virtual" type="Magento\Framework\Config\Data">
            <arguments>
                <argument name="cacheId" xsi:type="string"></argument>
            </arguments>
        </virtualType>
    </config>

    We now have confirmed the argument configuration is present. To check its value, one more line is needed.

    $this->assertSame('mage2kata_config_unitconversion_map', $arguments[$argumentName]);

    The test fails. To make it pass, set the value in the XML configuration.
    All green again? Good. Time to refactor.

    Lets extract a method assertDiArgumentSame().

    /**
     * @param string $expected
     * @param string $type
     * @param string $argumentName
     */
    private function assertDiArgumentSame($expected, $type, $argumentName)
    {
        $arguments = $this->getDiConfig()->getArguments($type);
        if (!isset($arguments[$argumentName])) {
            $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
        }
        $this->assertSame($expected, $arguments[$argumentName]);
    }
     
    public function testUnitMapConfigDataVirtualType()
    {
        $type = Model\Config\Data\Virtual::class;
        $this->assertVirtualType(\Magento\Framework\Config\Data::class, $type);
        $this->assertDiArgumentSame('mage2kata_config_unitconversion_map', $type, 'cacheId');
    }

    For the final test in this kata, lets check that the __construct() argument $reader is configured to a custom config data reader for our virtual type.

    We start almost the same as for did for asserting the cacheId argument.

    $argumentName = 'reader';
    $arguments = $this->getDiConfig()->getArguments($type);
    if (!isset($arguments[$argumentName])) {
        $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
    }

    All that is required to make this pass is to add one line to the di.xml file.

    As a first step at this point in the kata, I tend to duplicate the cacheId argument configuration and only change the argument name to reader.

    <argument name="cacheId" xsi:type="string">mage2kata_config_unitconversion_map</argument>
    <argument name="reader" xsi:type="string">mage2kata_config_unitconversion_map</argument>

    Back to green. Lets make the test fail again, this time by adding a check that will force us to set the type to object. For DI arguments of type object, the argument configuration value is a sub-array with an instance key. So we can use that for our check.

    $argumentName = 'reader';
    $arguments = $this->getDiConfig()->getArguments($type);
    if (!isset($arguments[$argumentName])) {
        $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
    }
    if (!isset($arguments[$argumentName]['instance'])) {
        $this->fail(sprintf('The argument "%s" for %s is not of xsi:type="object"', $argumentName, $type));
    }

    This test will pass once the reader argument type in the XML is adjusted accordingly.

    <argument name="cacheId" xsi:type="string">mage2kata_config_unitconversion_map</argument>
    <argument name="reader" xsi:type="object">mage2kata_config_unitconversion_map</argument>

    Finally we can add the assertion to the test that checks the expected reader instance class name.

    $this->assertSame(Model\Config\Data\Reader\Virtual::class, $arguments[$argumentName]['instance']);

    And the final version of the configuration that makes all three checks pass is as follows.

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="Mage2Kata\DiConfig\Model\Config\Data\Virtual" type="Magento\Framework\Config\Data">
            <arguments>
                <argument name="cacheId" xsi:type="string">mage2kata_config_unitconversion_map</argument>
                <argument name="reader" xsi:type="object">Mage2Kata\DiConfig\Model\Config\Data\Reader\Virtual</argument>
            </arguments>
        </virtualType>
    </config>

    Now that all our tests are passing, we can refactor the testing code.

    The final version of the test class looks like this:

    <?php
     
    namespace Mage2Kata\DiConfig;
     
    use Magento\Framework\ObjectManager\ConfigInterface as ObjectManagerConfig;
    use Magento\TestFramework\ObjectManager;
     
    class DiConfigConfigurationTest extends \PHPUnit_Framework_TestCase
    {
        /**
         * @return ObjectManagerConfig
         */
        private function getDiConfig()
        {
            return ObjectManager::getInstance()->get(ObjectManagerConfig::class);
        }
     
        /**
         * @param string $expectedType
         * @param string $type
         */
        private function assertVirtualType($expectedType, $type)
        {
            $this->assertSame($expectedType, $this->getDiConfig()->getInstanceType($type));
        }
     
        /**
         * @param string $expected
         * @param string $type
         * @param string $argumentName
         */
        private function assertDiArgumentSame($expected, $type, $argumentName)
        {
            $arguments = $this->getDiConfig()->getArguments($type);
            if (!isset($arguments[$argumentName])) {
                $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
            }
            $this->assertSame($expected, $arguments[$argumentName]);
        }
     
        /**
         * @param string $expectedType
         * @param string $type
         * @param string $argumentName
         */
        private function assertDiArgumentInstance($expectedType, $type, $argumentName)
        {
            $arguments = $this->getDiConfig()->getArguments($type);
            if (!isset($arguments[$argumentName])) {
                $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
            }
            if (!isset($arguments[$argumentName]['instance'])) {
                $this->fail(sprintf('The argument "%s" for %s is not of xsi:type="object"', $argumentName, $type));
            }
            $this->assertSame($expectedType, $arguments[$argumentName]['instance']);
        }
     
        public function testUnitMapConfigDataVirtualType()
        {
            $type = Model\Config\Data\Virtual::class;
            $this->assertVirtualType(\Magento\Framework\Config\Data::class, $type);
            $this->assertDiArgumentSame('mage2kata_config_unitconversion_map', $type, 'cacheId');
            $this->assertDiArgumentInstance(Model\Config\Data\Reader\Virtual::class, $type, 'reader');
        }
    }

    There still is a bit of code duplication, namely checking for the presence of a given argument.
    Sometimes I extract that into it’s own method, too, sometimes I don’t. I haven’t made up my mind yet which I like better.

    This completes the Di Config Kata.
    In the next kata we will continue to build the code for using a custom configuration XML file.

    There are many other nuances of DI configuration, like array or other scalar argument types or preferences, but this kata should give us the edge to be able to test whatever is needed.

    The final step of th kata is to delete all the code we have written, so the kata can be practiced again.

    Please leave a comment either on youtube, here on this page or ping me on twitter. It is always good to get feedback!

    I hope you enjoyed it, thanks for following along!

    comments powered by Disqus