Debugging the Magento 2 integration test setup

Continues Integration, Integration test, Magento, Magento 2, Testing

You might have been at the same point where i have been a few times: You want to start with integration tests on an existing Magento 2 store. You follow the steps to setup your IDE, start the first test and wait. After a while you get one of these nasty non descriptive errors:

Base table or view not found: 1146 Table 'database-name-test.store_website' doesn't exist

Base table or view not found: 1146 Table 'database-name-test.core_config_data' doesn't exist

But why is this happening? And how should we fix this?

Why is this happening?

To fully understand the problem and its solution we need to begin at the start. When you run the integration testsuite Magento actually runs the bin/magento setup:install command for you on the empty database. This means that your code must be installable on an empty database.

In theory this should be the case for every store, but in practice this never happens. All code is developed and executed in the context of an existing database. So when the integration tests are run, the setup is run from a different context.

The errors above are caused by the fact that Magento tries to access the store_website and core_config_data tables before they even are created. So the error, makes sense, but why is this happening?

For some reason, during the setup:install process the list of CLI commands is called. One of these commands has a dependency on the StoreManagerInterface, which is a pretty common dependency. This tries to fetch database from the database from its constructor, which is bad. We can avoid calling its constructor by using proxies.

Deep dive into the Object manager

This error is caused when Magento tries to initialize the \Magento\Framework\Console\CommandListInterface. When we register a new command we basically add our command to the constructor of its implementation:

<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="my_command_name" xsi:type="object">VENDOR\MODULE\Command\IMPLEMENTATION</item>
        </argument>
    </arguments>
</type>

 When the object manager is called to initialize a class, every dependency is resolved. This means that when you have 100 commands, Magento tries to initialize 100 classes. So when we find the loop where this happens we can find the error.

This loop is located in:

\Magento\Framework\ObjectManager\Factory\AbstractFactory::parseArray

The line where this is failing is the one that contains:

$array[$key] = $this->objectManager->get($item['instance']);

There are 2 ways to find the failing class:

  • Put an xdebug breakpoint on the line and keep pressing F8 until the error happens. Big downside of this is that you need to press this dozens of times and at the moment this happens you can't see the current command anymore.
  • Put a temporary try/catch block around it. This is easier, but you have to edit the code in de vendor folder for this.

For option 2: Replace the line by this code:

try {
    $array[$key] = $this->objectManager->get($item['instance']);
} catch (\Exception $exception) {
    throw new \Exception('Failing command: ' . $key . ' instance: ' . $item['instance']);
}

You need to throw an exception as an echo get's eaten by the parent command. Also please note: Remove this code when you're done!. This is for development only.

Tip: You need to delete the contents of the dev/tests/integration/tmp folder between runs, otherwise the error will change and to no help.

Fixing the bug

When you have found the command that is causing this error we can finally fix this error. Fixing the bug can be challeging too, as it's not always obvious which dependency is causing this issue. You might have dependencies which have dependencies which have dependencies which are relying on the StoreManager, so i may be nested. Basically there are 2 ways to fix this kind of bugs:

  • Add the Magento_Store module to your modules sequence in etc/module.xml.
  • Replace all arguments with proxies. This will speed up your bin/magento calls a little bit too.

 There are 2 ways to switch the classes for proxies:

  • By appending \Proxy to the classname in the constructor, which is good for debugging.
  • By appending \Proxy to the classname in di.xml, which is the recommended way, plus, you are able to do this for other third party modules you can't alter directly. This looks likes this:
<type name="<VENDOR>\<MODULE>\Console\Command\SuperImportantImport">
    <arguments>
        <argument name="importService" xsi:type="object">\<VENDOR>\<MODULE>\Service\Import\SuperImportantService</argument>
    </arguments>
</type>

The sequence part is easier. From:

<module name="<VENDOR>_<MODULE>" setup_version="1.0.0" />

To:

<module name="<VENDOR>_<MODULE>" setup_version="1.0.0">
    <sequence>
        <module name="Magento_Store"/>
    </sequence>
</module>

Please note that you might need to repeat this process a few times as there might be multiple classes that fail. So:

  • Run test, fail.
  • Fix broken class.
  • Delete dev/tests/integration/tmp/*.
  • Repeat.

Common issues

I've encountered some issues while running this code:

  • I needed to delete the dev/tests/integration/tmp/* folder.
  • You can't run the command using Magerun. You need to use bin/magento setup:upgrade.
  • In some cases you may get the error Invalid entity_type specified: catalog_product. Check which module gives this error, and add Magento_Catalog to the sequence of that module.
Write a comment

Your email address will not be published. Required fields are marked*