Run PHPUnit Integration Tests and Coverage from PHPStorm for Magento
AI generated
IDE
{ }
PHPUnit · Magento 2 · Coverage · PHPStorm
Run PHPUnit Integration Tests and Coverage
from PHPStorm for Magento

Magento 2 ships its own test infrastructure that differs substantially from a standard PHPUnit project. If you want to run integration tests directly from PHPStorm, see coverage reports in the IDE, and debug individual test methods with a single click, you need specific configuration, which this article covers in full.

20 min read PHPUnit · Xdebug Coverage · Run Configurations Magento 2.4.8 · PHP 8.4 · PHPStorm 2024+

1. Understanding the Magento test infrastructure

Magento 2 distinguishes three types of tests that differ fundamentally in effort, runtime, and infrastructure requirements. Unit tests test individual classes in isolation, without database access and without a Magento bootstrap, so they run in seconds. Integration tests need a fully installed Magento instance with a dedicated test database and can take minutes. Functional tests (MFTF) need a running browser and a web server, so they belong in CI pipelines, not in the IDE workflow.

The Magento test framework is built on PHPUnit but extends it with its own base classes. Integration tests inherit from Magento\TestFramework\TestCase\AbstractController or Magento\TestFramework\TestCase\AbstractIntegrationTestCase, not from PHPUnit\Framework\TestCase. These base classes bootstrap Magento before every test, roll back the database afterward, and provide the ObjectManager. PHPStorm can run all of this directly, but the run configuration must point to the correct phpunit.xml and the correct working directory.

2. Prerequisites: test database and environment variables

Integration tests need a separate database. It must not be the production or development database, because the tests manipulate data after each test run and clean up via transaction rollbacks. By convention, the database name is magento_integration_tests. The file dev/tests/integration/etc/install-config-mysql.php configures the database connection for integration tests. This file is not version controlled; each developer maintains their own local version.

In a Docker setup, the test database must be created inside the MySQL container: docker exec db mysql -uroot -p -e "CREATE DATABASE magento_integration_tests CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;". Afterward, install-config-mysql.php must be filled in with the container-internal database credentials. Inside the PHP container, the MySQL host is the service name db, not localhost. This is a common mistake: copying the host address from the regular .env file, when the container resolves hostnames differently than the host machine does.


<?php
// dev/tests/integration/etc/install-config-mysql.php
// DO NOT COMMIT (contains local database credentials)
return [
    'db-host'           => 'db',          // Docker service name, not localhost
    'db-user'           => 'magento',
    'db-password'       => 'magento',
    'db-name'           => 'magento_integration_tests',
    'db-prefix'         => '',
    'backend-frontname' => 'backend',
    'admin-user'        => \Magento\TestFramework\Bootstrap::ADMIN_NAME,
    'admin-password'    => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD,
    'admin-email'       => 'admin@example.com',
    'admin-firstname'   => 'Admin',
    'admin-lastname'    => 'Test',
    'amqp-host'         => '',
    'amqp-port'         => '',
    'elasticsearch-host' => 'opensearch',
    'elasticsearch-port' => '9200',
];

3. Configuring phpunit.xml for unit and integration tests

Magento ships a phpunit.xml.dist for integration tests under dev/tests/integration/. This file should be copied and adapted as phpunit.xml; the copy is also not version controlled. In phpunit.xml you configure test suite paths, coverage filters, and environment variables. The key difference from a standard PHPUnit configuration: the bootstrap attribute points to dev/tests/integration/framework/bootstrap.php, which kicks off the entire Magento bootstrap process.

For unit tests, there's a separate phpunit.xml.dist under dev/tests/unit/. Unit tests run without database access and without a bootstrap, so they're significantly faster and suit quick feedback loops during development. PHPStorm can hold both test suites as separate run configurations: one for fast unit tests on save, one for integration tests before commit. The coverage report filter is controlled via <include> paths in phpunit.xml; only your own modules should be measured, not the Magento core.

4. Creating run configurations in PHPStorm

PHPUnit run configurations are created in PHPStorm under Run → Edit Configurations → + → PHPUnit. For integration tests: set the test scope to Defined in the configuration file, enter dev/tests/integration/phpunit.xml as the configuration file, select the remote interpreter (Docker Compose phpfpm), and set the working directory to the Magento root inside the container: /var/www/html. Environment variables from the compose file are inherited automatically.

For unit tests, create a second configuration with dev/tests/unit/phpunit.xml. Here you can also choose Test scope → Directory with the path to your own module, to run only the tests of the module currently being developed. PHPStorm shows all run configurations in the toolbar and allows quick switching between them. The keyboard shortcut Ctrl+Shift+F10 runs the test of the currently open class directly, without having to select a run configuration.


<?php
// dev/tests/integration/phpunit.xml (relevant configuration sections)
/*
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="..."
         colors="true"
         columns="80"
         bootstrap="./framework/bootstrap.php"
         stderr="true">

  <testsuites>
    <!-- Custom module tests only (faster than full suite) -->
    <testsuite name="Mironsoft_Module Integration Tests">
      <directory>../../../app/code/Mironsoft/*/Test/Integration</directory>
    </testsuite>
  </testsuites>

  <coverage>
    <include>
      <directory suffix=".php">../../../app/code/Mironsoft</directory>
    </include>
    <exclude>
      <directory>../../../app/code/Mironsoft/*/Test</directory>
    </exclude>
  </coverage>

  <php>
    <ini name="memory_limit" value="-1" />
    <ini name="date.timezone" value="Europe/Berlin" />
  </php>
</phpunit>
*/

5. Enabling and reading code coverage with Xdebug

Code coverage for PHPUnit is triggered in PHPStorm via the Run-With-Coverage button, the green play icon with a shield. PHPStorm automatically enables Xdebug in coverage mode and collects line coverage data during the test run. Prerequisite: Xdebug must be configured with xdebug.mode=debug,coverage. The coverage report appears in the Coverage panel after the test run and shows classes, methods, and lines with colored highlighting directly in the editor.

Green lines are covered by at least one test, red lines are untested, and yellow lines are partially covered (for example, one branch of an if statement). PHPStorm can also export HTML coverage reports as separate files: Run → Generate Coverage Report. These reports are useful for CI artifacts and code reviews. Important: coverage measurement slows down the test run considerably, so run tests without coverage for fast feedback loops and enable coverage only for analysis runs.

6. Running individual tests and methods directly from the editor

One of the biggest advantages of PHPStorm's integration: PHPUnit test methods can be run directly from the code editor. A green play icon appears in the gutter next to every test class and test method. Clicking it runs exactly that test with the most recently used run configuration. Right-clicking also offers Debug, which enables breakpoints in the test method and in the code under test.

With Ctrl+Shift+T, PHPStorm jumps from a class to its test class and back. This is especially useful with a test-driven-development approach: write the test class, write the code, switch between the two with a single keystroke. PHPStorm also creates missing test classes automatically via Navigate → Test and sets up the correct directory structure (Test/Unit/, Test/Integration/) automatically, provided the module namespacing is configured correctly.

7. Writing a complete Magento integration test class

Magento integration tests differ from unit tests through access to the ObjectManager, the database, and the entire Magento kernel. ObjectManager access works via Magento\TestFramework\Helper\Bootstrap::getObjectManager(). The @magentoDbIsolation enabled annotation ensures that all database operations run inside a transaction and are rolled back after the test. With @magentoDataFixture, test data can be loaded from fixture files.

A common mistake when writing integration tests: instantiating services with new ClassName() instead of going through the ObjectManager. This bypasses Magento's DI and leads to errors because dependencies are missing. The correct approach is $this->objectManager->get(ServiceInterface::class) or $this->objectManager->create(ServiceInterface::class). get returns the singleton instance, create always produces a new instance, so pick the right one depending on what the test requires.


<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Integration\Model;

use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\TestCase\AbstractIntegrationTestCase;
use Mironsoft\Catalog\Api\ProductEnricherInterface;
use Mironsoft\Catalog\Model\ProductEnricher;

/**
 * Integration test for ProductEnricher service.
 *
 * @magentoDbIsolation enabled
 * @magentoAppIsolation enabled
 */
class ProductEnricherTest extends AbstractIntegrationTestCase
{
    private ProductEnricherInterface $enricher;

    protected function setUp(): void
    {
        parent::setUp();
        $this->enricher = Bootstrap::getObjectManager()->get(ProductEnricherInterface::class);
    }

    /**
     * @magentoDataFixture Mironsoft_Catalog::Test/Integration/_files/simple_product.php
     */
    public function testEnrichAddsExpectedAttributes(): void
    {
        $productId = 1;
        $result    = $this->enricher->enrich($productId);

        self::assertNotNull($result, 'Enriched product must not be null');
        self::assertTrue($result->hasCustomAttribute('mironsoft_enriched'));
        self::assertEquals('1', $result->getCustomAttribute('mironsoft_enriched')->getValue());
    }

    /**
     * @magentoDataFixture Mironsoft_Catalog::Test/Integration/_files/simple_product.php
     */
    public function testEnrichThrowsForInvalidProduct(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->enricher->enrich(99999);
    }
}

8. Unit tests vs. integration tests compared

Choosing between unit and integration tests in Magento is not a dogmatic decision, it depends on what you want to test. Business logic without database access belongs in unit tests. Repositories, events, plugins, and commands that interact with the Magento infrastructure belong in integration tests. Both types of tests share the PHPUnit framework and PHPStorm integration; the difference lies in runtime, infrastructure requirements, and test depth.

Criterion Unit Test Integration Test Recommendation
Runtime Milliseconds Seconds to minutes Unit tests in the development flow
Database access None (mocks) Yes, dedicated DB Repositories → integration test
Magento bootstrap Not required Full DI/plugin behavior → integration test
Coverage depth Within class Across modules Both for full coverage
PHPStorm start Instant from editor Run config required Both controllable via PHPStorm

The practical recommendation: unit tests cover the business logic of value objects, calculator classes, and service implementations. Integration tests verify that plugins fire correctly, that repository methods persist the right data, and that events call the expected observers. Both types of tests complement each other: if you only have unit tests, you're not testing the integration; if you only have integration tests, your feedback loops are too slow.

9. Common errors and their causes

The most common error when first running integration tests from PHPStorm: Error: Class not found for Magento classes. Cause: the run configuration's working directory doesn't point to the Magento root, or the autoloader wasn't wired up correctly. Solution: set the working directory in the run configuration to /var/www/html (the container path) and make sure phpunit.xml contains the correct bootstrap path.

Another common error: the database connection fails. The error message often contains Connection refused on host localhost. Cause: install-config-mysql.php contains localhost instead of db (the Docker service name). Inside the container, the MySQL host is the service name from the compose file, not localhost. After the fix, the integration test bootstrap needs to run again; PHPStorm offers the option Invalidate Caches and Restart in the File menu for this.

10. Summary

Running PHPUnit integration tests for Magento from PHPStorm takes four steps: create the test database and fill in install-config-mysql.php with container-internal credentials, adapt phpunit.xml with your own test suite filter, create run configurations in PHPStorm with a remote interpreter and the correct working directory, and enable Xdebug in coverage mode. Each step is a one-time setup per project; after that, tests run with a single click or keystroke directly from the IDE.

The payoff is lasting: coverage reports immediately show which lines in your own module are untested. Individual tests can be debugged with breakpoints, without opening a terminal. Switching between implementation and test class with Ctrl+Shift+T speeds up the TDD workflow considerably. In Magento projects, where modules interact in complex ways, IDE-driven test execution isn't a convenience, it's a fundamental quality assurance tool.

PHPUnit + Magento in PHPStorm: The Essentials at a Glance

Test database

Create magento_integration_tests. Configure install-config-mysql.php with the Docker service name db as the host, not localhost.

Run configuration

Remote interpreter (phpfpm), working directory /var/www/html, configuration file dev/tests/integration/phpunit.xml.

Coverage

xdebug.mode=debug,coverage. The Run-With-Coverage button enables coverage automatically. The report appears inline in the editor.

Single test

The play icon next to a test method runs exactly that test. Ctrl+Shift+T switches between a class and its test class.

Mironsoft

Magento testing, code quality, and PHPStorm integration

Need Magento tests set up in PHPStorm?

We set up unit and integration tests for your Magento module, with coverage reports, run configurations, and full PHPStorm integration.

Test setup

Set up the test database, phpunit.xml, and run configurations for your Magento project

Test development

Write unit and integration tests for existing modules and increase coverage

CI integration

Integrate PHPUnit into GitHub Actions or GitLab CI with coverage reporting

11. FAQ: PHPUnit and Coverage in PHPStorm for Magento

1Run integration tests directly from PHPStorm?
Yes, with a remote interpreter, working directory /var/www/html, and the correct phpunit.xml path set in the run configuration.
2Why a separate test database?
Integration tests write and delete data. Without a separate DB, development data would get overwritten. Transaction rollback cleans up after every test.
3DbIsolation vs. AppIsolation?
DbIsolation: a transaction per test. AppIsolation: kernel restart after the test (slow). AppIsolation is only needed for static state changes.
4Enable code coverage?
Click the Run-With-Coverage button. xdebug.mode=debug,coverage must be set. The report appears inline in the editor with green and red lines.
5Run a single test method?
Click the play icon next to the method, or place the cursor inside the method and press Ctrl+Shift+F10.
6Why does the bootstrap fail?
Wrong DB host (localhost instead of db), missing install-config-mysql.php, wrong working directory, or a missing test database.
7Fixtures in PHP files?
Yes. @magentoDataFixture with a path to the PHP file under Test/Integration/_files/. It runs before the test.
8Switch between class and test class?
Ctrl+Shift+T opens a dialog for switching to or creating the test class. PHPStorm recognizes Test/Unit/ and Test/Integration/ automatically.
9Does coverage slow down tests?
Yes, 3 to 10 times slower. For fast cycles, run without coverage. Enable coverage only for analysis runs and CI.
10Filter coverage to your own modules?
In phpunit.xml under <coverage><include>, enter only app/code/Mironsoft. Exclude Magento core, otherwise the report becomes cluttered and measurement takes very long.