*** Check out Grokking Magento ***

Vinai Kopp

Magento Expert, Developer & Trainer

  • 11. The Custom Config File Edge2Edge Test Kata

    June 17, 2016

    Mage2Katas

    In this episode we are going to round up and finish the custom config file mini-series. We are going to do that by adding 2 more tests, and these tests will cover the whole process of using our custom config file from beginning to end.
    We are going to use the code in the same way it is going to be used in the real application. And if you where following along the series, then these tests will actually expose a bug we that still is present in the current configuration. This illustrates nicely how important it is to have edge to edge tests.

    This final episode of the custom config file kata series requires the code of the previous ones.
    If you don’t have it handy, I suggest doing the following katas again and then move into this one. That way all the previous content will also be fresh on your mind.

    The only additional step that was not covered by the previous kata was adding the converter to the DI argument configuration for the config reader virtual type.

    <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual" type="Magento\Framework\Config\Reader\Filesystem">
        <arguments>
            <argument name="fileName" xsi:type="string">unit_conversion.xml</argument>
            <argument name="schemaLocator" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversion\SchemaLocator\Virtual</argument>
            <argument name="converter" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversionConfigConverter</argument>
        </arguments>
    </virtualType>

    I also added the appropriate tests to the unit conversion config integration test method for the config reader, but because they are so similar to the ones already present I’ll won’t include them in this post.

    Lets get started by creating a new test class Edge2EdgeUnitConversionConfigTest.
    I would recommend adding a testNothing() test method to begin, but once it is confirmed our test can be executed, the next step I do is to check the config class can be used to read a conversion factor from our configuration unit_conversion.xml file.

    class Edge2EdgeUnitConversionConfigTest extends \PHPUnit_Framework_TestCase
    {
        private $configType = \Mage2Kata\DiConfig\Model\Config\UnitConversion\Virtual::class;
     
        public function testCanAccessUnitConversionConfig()
        {
            $objectManager = ObjectManager::getInstance();
            /** @var \Magento\Framework\Config\Data $config */
            $config = $objectManager->create($this->configType);
            $this->assertSame('2.20462257811', $config->get('kg/lbs'));
        }
    }

    This test succeeds directly. Usually I do not write tests that I know succeed. But to be honst, I wasn’t 100% sure, so it is good to have a test in place that puts it all together.

    One of the key aspects of working with Magento is configuration merging.
    That is the one thing we have not actually done yet. So far we have only worked with a single unit_conversion.xml file.

    The question is, how can we supply a second unit_conversion.xml file to be merged? The config reader builds the list of files based on the file name, and we already have a file with that name in our module.
    Do we have to create a second module, just so we can test the merging result?

    Lets have another look at the Config\Reader\Filesystem class to see if there is a better way.

    In the read() method we find the following line:

    $fileList = $this->_fileResolver->get($this->_fileName, $scope);

    A bit later, in the method _readFiles(), the class the interates over the $fileList:

    foreach ($fileList as $key => $content) {
        ...
        $configMerger->merge($content);
        ...
        new \Magento\Framework\Phrase("Invalid XML in file %1:\n%2", [$key, $e->getMessage()])
        ...
    }

    In the loop we can see the $content is merged, and in the line that builds an exception we can see that the $key contains the file name.
    From this we can conduct that the $_fileReader->get() method returns a map of file names to file content.

    Lets try to replace the file reader with a test double that returns a hardcoded list of files and content.
    This will allow us to test the merge process without having to create another real file in the file system.

    I would like to keep the first test method since it documents the class usage nicely. So lets add a new test method to check the merging.

    To get started, lets get the mock working with a single fake file.
    Once that works, we can add the second fake file to check the merge process.

    The following is the new test method as work-in-progress, with only one fake file.

    public function testMultipleFilesCanBeMerged()
    {
        $mockFileResolver = $this->getMock(\Magento\Framework\Config\FileResolverInterface::class);
        $mockFileResolver->method('get')->willReturn([
            'test1.xml' => <<<XML
    <conversion_map>
        <unit id="kg" type="weight">
            <conversion to="lbs" factor="333"/>
        </unit>
    </conversion_map>
    XML
        ]);
        $objectManager = ObjectManager::getInstance();
        $reader = $objectManager->create(
            \Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual::class,
            ['fileResolver' => $mockFileResolver]
        );
        $mockCache = $this->getMock(\Magento\Framework\Config\CacheInterface::class);
        $mockCache->method('load')->willReturn(false);
        /** @var \Magento\Framework\Config\Data $config */
        $config = $objectManager->create($this->configType, ['reader' => $reader, 'cache' => $mockCache]);
        $this->assertSame('333', $config->get('kg/lbs'));
    }

    As you can see in the video, creating this state of the method actually took several iterations.
    Besides the mock file resolver, we also have to stub the config cache instance, so the reader doesn’t use the cached content of the real file instead of our fake file content array.

    Once the mocks are ready, we inject the FileResolver test double into the Config\Reader\Filesystem instance, and then we inject the reader and the stub cache into our config instance.

    This is a nice example how partially specifying the object graph during an integration test can make testing a lot simpler.

    Once the test method reaches the above state, the test passes. So finally it’s time to add the second fake file.

        $mockFileResolver->method('get')->willReturn([
            'test1.xml' => <<<XML
    <conversion_map>
        <unit id="kg" type="weight">
            <conversion to="lbs" factor="333"/>
        </unit>
    </conversion_map>
    XML
            ,'test2.xml' => <<<XML
    <conversion_map>
        <unit id="kg" type="weight">
            <conversion to="lbs" factor="444"/>
        </unit>
    </conversion_map>
    XML
        ]);

    Run the test and…. BAM!

    More then one node matching the query: /conversion_map/unit/conversion

    The config merger doesn’t know how to merge the two files. In Magento 1 config merging always happened based on node names. In Magento 2 this is different. Specifc node attributes define if a node should be merged with an existing one, or if it should be added to the config data as a new record.

    We have to tell the merger which attributes serve as such unique identifiers for every XML node type in the file.
    This can be done by specifying the $idAttributes constructor argument for the FileSystemReader in our etc/di.xml file.

    The following argument configuration needs to be added to the config reader virtual type in the etc/di.xml:

    <argument name="idAttributes" xsi:type="array">
        <item name="/conversion_map/unit" xsi:type="string">id</item>
        <item name="/conversion_map/unit/conversion" xsi:type="string">to</item>
    </argument>

    The <item> node names will be used as the array keys, and the node values will become the array values in the array that is passed into the constructor.

    If necessary we have to clear the test sandbox cache directory so the config file change will be visible to Magento.

    Run the test… and we are green!

    And with this we have a complete working solution for a custom XML configuration file.

    In a real project, the tests we wrote today arguably make the earlier unit conversion integration tests obsolete. We could delete them, since all they check is also covered by the tests we have in the edge 2 edge test.

    Deleting integration tests that don’t provide unique value is one way to keep the integration test suite runtime at an acceptable time.

    However, since this is a code kata and not code that will be used in a real project, lets go ahead and delete it all. Time to reflect on what learning could be taken away from this session, so we can apply it the next time we do the kata.

    Please let me know what you think, if you have comments, questions or critique.

    Thanks for reading and until next time: happy testing!


    Postscriptum:

    In case something is not working for you as it does in the video, here is my version of the complete edge to edge test and di.xml.
    (The result actually is slightly different every time I do the katas.)

    <?php
     
    namespace Mage2Kata\DiConfig;
     
    use Magento\TestFramework\ObjectManager;
     
    class Edge2EdgeUnitConversionConfigTest extends \PHPUnit_Framework_TestCase
    {
        private $configType = \Mage2Kata\DiConfig\Model\Config\UnitConversion\Virtual::class;
     
        public function testCanAccessUnitConversionConfig()
        {
            $objectManager = ObjectManager::getInstance();
            /** @var \Magento\Framework\Config\Data $config */
            $config = $objectManager->create($this->configType);
            $this->assertSame('2.20462257811', $config->get('kg/lbs'));
        }
     
        public function testMultipleFilesCanBeMerged()
        {
            $mockFileResolver = $this->getMock(\Magento\Framework\Config\FileResolverInterface::class);
            $mockFileResolver->method('get')->willReturn([
                'test1.xml' => <<<XML
    <conversion_map>
        <unit id="kg" type="weight">
            <conversion to="mg" factor="111"/>
            <conversion to="g" factor="222"/>
            <conversion to="lbs" factor="333"/>
        </unit>
    </conversion_map>
    XML
                ,'test2.xml' => <<<XML
    <conversion_map>
        <unit id="kg" type="weight">
            <conversion to="lbs" factor="444"/>
        </unit>
    </conversion_map>
    XML
            ]);
            $objectManager = ObjectManager::getInstance();
            $reader = $objectManager->create(
                \Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual::class,
                ['fileResolver' => $mockFileResolver]
            );
            $mockCache = $this->getMock(\Magento\Framework\Config\CacheInterface::class);
            $mockCache->method('load')->willReturn(false);
            /** @var \Magento\Framework\Config\Data $config */
            $config = $objectManager->create($this->configType, ['reader' => $reader, 'cache' => $mockCache]);
            $this->assertSame('444', $config->get('kg/lbs'));
        }
    }
    <?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\UnitConversion\Virtual" type="Magento\Framework\Config\Data">
            <arguments>
                <argument name="cacheId" xsi:type="string">mage2kata_unitconversion_map_config</argument>
                <argument name="reader" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual</argument>
            </arguments>
        </virtualType>
        <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual" type="Magento\Framework\Config\Reader\Filesystem">
            <arguments>
                <argument name="fileName" xsi:type="string">unit_conversion.xml</argument>
                <argument name="schemaLocator" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversion\SchemaLocator\Virtual</argument>
                <argument name="converter" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversionConfigConverter</argument>
                <argument name="idAttributes" xsi:type="array">
                    <item name="/conversion_map/unit" xsi:type="string">id</item>
                    <item name="/conversion_map/unit/conversion" xsi:type="string">to</item>
                </argument>
            </arguments>
        </virtualType>
        <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\SchemaLocator\Virtual" type="Magento\Framework\Config\GenericSchemaLocator">
            <arguments>
                <argument name="moduleName" xsi:type="string">Mage2Kata_DiConfig</argument>
                <argument name="schema" xsi:type="string">unit_conversion.xsd</argument>
            </arguments>
        </virtualType>
    </config>
    comments powered by Disqus

Read Grokking Magento now!

"The best technical reading I had since years by @VinaiKopp"

Tim Bezhashvyly
Magento Developer