Creating Integration Tests for Magento 2 Modules

This works for us but we haven't yet looked into moving them to a separate location to address 6.)

1.) Place your integration tests under dev/tests/integration/testsuite/Vendor
2.) copy dev/tests/integration/phpunit.dist.xml
to
dev/tests/integration/phpunit.xml

and replace

        <directory suffix="Test.php">testsuite</directory>
        <directory suffix="Test.php">../../../update/dev/tests/integration/testsuite</directory>
        <exclude>testsuite/Magento/Test/Integrity</exclude>
        <exclude>testsuite/Magento/MemoryUsageTest.php</exclude>

with

        <directory suffix="Test.php">testsuite/Vendor</directory>

3.) run it ../../../vendor/bin/phpunit or with ../../../vendor/bin/phpunit path/to/tests from the dev/test/integration folder

Please note that the integration tests take longer than 15 seconds, at least on first run as it essentially installs Magento. You can save on subsequent runs if you use

<const name="TESTS_CLEANUP" value="disabled"/>

in your phpunit.xml


I have played a little bit with the integration tests, and this is what I found so far.

Basically, I've followed similar steps to what Fooman said, with some differences in order to make the integration test be part of my module.

These are the steps I followed:

1- Place your integration tests under app/code/Vendor/CustomModule/Test/Integration

2- Copy dev/tests/integration/phpunit.dist.xml to dev/tests/integration/phpunit.xml

and replace

<testsuite name="Magento Integration Tests">
    <directory suffix="Test.php">testsuite</directory>
    <directory suffix="Test.php">../../../update/dev/tests/integration/testsuite</directory>
    <exclude>testsuite/Magento/Test/Integrity</exclude>
    <exclude>testsuite/Magento/MemoryUsageTest.php</exclude>
</testsuite>

with

<testsuite name="Magento Integration Tests">
    <directory suffix="Test.php">../../../app/code/Vendor/CustomModule/Test/Integration</directory>
</testsuite>

3- Then I run it using the CLI tool bin/magento dev:test:run integration

You should have in mind what Fooman says about the "TESTS_CLEANUP" and the time it takes to setup the integration tests if you have the cleanup enable.

Here I add a functional example for further reference. You will see how you can access the object manager, and generate instance of Magento classes, as well as using Magento fixtures.

app/code/Vendor/CustomModule/Controller/Order/Info.php

namespace Vendor\CustomModule\Controller\Order;

use Magento\Framework\Controller\ResultFactory;

class Info
    extends \Magento\Framework\App\Action\Action
{
    /**
     * @param \Magento\Framework\App\Action\Context $context
     * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
     */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
    )
    {
        $this->orderRepository = $orderRepository;
        parent::__construct($context);
    }

    /**
     * Return Json OrderInfo
     *
     * @return \Magento\Framework\Controller\Result\Json $this
     */
    public function execute()
    {
        $orderId = $this->getRequest()->getParam('id');
        $order = $this->orderRepository->get($orderId);
        $orderInfo = [
            'total' => $order->getBaseGrandTotal()
        ];

        /** @var \Magento\Framework\Controller\Result\Json $result */
        $result = $this->resultFactory->create(ResultFactory::TYPE_JSON);
        return $result->setData($orderInfo);
    }

}

app/code/Vendor/CustomModule/etc/frontend/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="vendor_custommodule" frontName="vendor_custommodule">
            <module name="Vendor_CustomModule"/>
        </route>
    </router>
</config>

app/code/Vendor/CustomModule/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_CustomModule" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Sales" />
        </sequence>
    </module>
</config>

app/code/Vendor/CustomModule/Test/Integration/Controller/Order/InfoTest.php

namespace Vendor\CustomModule\Controller\Order;

use Magento\TestFramework\TestCase\AbstractController;

class InfoTest extends AbstractController
{
    public function getOrderInfoActionDataProvider()
    {
        return [
            'order with one simple item' => [
                'incrementId' => '100000001',
                'contentType' => 'application/json',
                'orderTotal' => 100
            ]
        ];
    }

    /**
     * @dataProvider getOrderInfoActionDataProvider
     * @magentoDataFixture Magento/Sales/_files/order.php
     */
    public function testOrderInfoAction($incrementId, $expectedContentType, $expectedOrderTotal)
    {
        /** @var $objectManager \Magento\TestFramework\ObjectManager */
        $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();

        /** @var \Magento\Sales\Model\OrderFactory $orderFactory */
        $orderFactory = $objectManager->get('Magento\Sales\Model\OrderFactory');
        $order = $orderFactory->create();
        $order->loadByIncrementId($incrementId);

        $this->dispatch("vendor_custommodule/order/info/id/{$order->getId()}");

        $contentType = $this->getResponse()->getHeader('Content-Type');
        $this->assertEquals($expectedContentType, $contentType->getFieldValue());

        $responseJson = $this->getResponse()->getBody();
        $responseArray = json_decode($responseJson, true);
        $this->assertEquals($expectedOrderTotal, $responseArray['total']);
    }
}

app/code/Vendor/CustomModule/registration.php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_CustomModule',
    __DIR__
);

I've successfully placed my integration tests into a separate directory: src/My/Module/test/integration. It could be any other directory as well, like app/code/My/Module/Test.

Add them as new test suite to the Magento integration tests: Copy dev/tests/integration/phpunit.xml.dist to dev/tests/integration/phpunit.xml and add the following in the <testsuites> node:

<testsuite name="My_Module">
    <directory suffix="Test.php">../../../src/My/Module/test</directory>
</testsuite>

Then run the tests like this from the dev/tests/integration directory:

../../../vendor/bin/phpunit --testsuite "My_Module"

With the --testsuite parameter you can select one test suite by name, so that not all integration tests are run at once

Update: Fixtures

To use own fixtures, a little workaround was necessary, because in Magento\TestFramework\Annotation the fixture base directory is defined globally. But fortunately Magento allows method names as fixtures as well, so the following works:

/**
 * @magentoDataFixture loadFixture
 */
public function testSomething()
{
}

public static function loadFixture()
{
    include __DIR__ . '_files/something_fixture.php';
}