Integration Tests
Integration tests boot the real application against their own test database, which is why they catch the breakage mocks hide.
#[DataFixture(string $type, array $data = [], ?string $as = null, ?string $scope = null, int $count = 1)]An integration test runs against a real, installed Magento. The framework installs a store into a database you nominate, then runs your test with the full object manager available. No mocks of Magento are involved.
Setting up the test database
Copy dev/tests/integration/etc/install-config-mysql.php.dist to install-config-mysql.php and fill in the database and search engine details. The first run installs the store, which takes minutes; later runs reinstall it because TESTS_CLEANUP is enabled in phpunit.xml.dist. Set that constant to disabled in your own phpunit.xml while you iterate. Your tests go in Vendor/Module/Test/Integration and run from the dev/tests/integration directory.
Fixtures
A fixture is data created before the test and removed after it. Since 2.4.5 the current form is the #[DataFixture] attribute, naming a fixture class such as Magento\Catalog\Test\Fixture\Product, with an alias you later use to read the created record back out of DataFixtureStorageManager. The older @magentoDataFixture doc-comment annotation names a PHP file instead and still works in 2.4.9, because Magento parses it itself rather than relying on PHPUnit.
With #[DbIsolation(true)] the test runs inside a transaction that is rolled back, so one test cannot leak data into the next.
Why these find the real bugs
The installer applies your db_schema.xml, merges every di.xml, activates your plugins and builds the EAV tables. A missing db_schema_whitelist.json entry, a preference another module overrode, a plugin on a method interception cannot reach: all of them surface here and none of them can surface in a unit test.
Example
<?php
// app/code/Vendor/Loyalty/Test/Integration/Model/PointsLedgerTest.php
declare(strict_types=1);
namespace Vendor\Loyalty\Test\Integration\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
use Magento\Framework\ObjectManagerInterface;
use Magento\TestFramework\Fixture\DataFixture;
use Magento\TestFramework\Fixture\DataFixtureStorage;
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
use Magento\TestFramework\Fixture\DbIsolation;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
class PointsLedgerTest extends TestCase
{
private ObjectManagerInterface $objectManager;
private DataFixtureStorage $fixtures;
private ProductRepositoryInterface $productRepository;
protected function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
$this->fixtures = $this->objectManager
->get(DataFixtureStorageManager::class)
->getStorage();
$this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
}
#[DbIsolation(true)]
#[DataFixture(ProductFixture::class, ['sku' => 'loyalty-simple', 'price' => 25.40], 'product')]
public function testFixtureProductIsReadableThroughTheRepository(): void
{
$sku = $this->fixtures->get('product')->getSku();
$product = $this->productRepository->get($sku);
self::assertSame(25.40, (float)$product->getPrice());
}
}
// cd dev/tests/integration
// ../../../vendor/bin/phpunit ../../../app/code/Vendor/Loyalty/Test/Integration
// OK (1 test, 1 assertion)When to use it
- Proving a new db_schema.xml column exists after install, whitelist entry included.
- Checking a repository save really persists an EAV attribute value.
- Confirming a plugin actually intercepts the method you declared it on.
- Reproducing a customer-reported save failure against real catalog data.
More examples
The install config the suite cannot start without
Copy the .dist file and change the database name to something you are willing to lose. These keys are the arguments the framework passes to setup:install, so the search engine entry matters: without a reachable OpenSearch the install itself fails and every test errors before any assertion runs. The amqp keys are only needed if you have message queue tests. No command applies this file; it is read at the start of the next phpunit run.
<?php
// dev/tests/integration/etc/install-config-mysql.php
declare(strict_types=1);
use Magento\TestFramework\Bootstrap;
return [
'db-host' => 'localhost',
'db-user' => 'root',
'db-password' => '123123q',
'db-name' => 'magento_integration_tests',
'db-prefix' => '',
'backend-frontname' => 'backend',
'search-engine' => 'opensearch',
'opensearch-host' => 'localhost',
'opensearch-port' => 9200,
'admin-user' => Bootstrap::ADMIN_NAME,
'admin-password' => Bootstrap::ADMIN_PASSWORD,
'admin-email' => Bootstrap::ADMIN_EMAIL,
'admin-firstname' => Bootstrap::ADMIN_FIRSTNAME,
'admin-lastname' => Bootstrap::ADMIN_LASTNAME,
'amqp-host' => 'localhost',
'amqp-port' => '5672',
'amqp-user' => 'guest',
'amqp-password' => 'guest',
'consumers-wait-for-messages' => '0',
];The older annotation form, which you will still read in core
These three annotations are the long-standing equivalents of the #[DbIsolation], #[DataFixture] and #[Config] attributes, and they do the same work: the attribute form is the current one, the doc-comment form is what most core tests still use. The fixture path is relative to dev/tests/integration/testsuite. Worth knowing precisely because PHPUnit 12 dropped its own doc-comment metadata in 2.4.9, which makes it easy to assume these stopped working too. They did not, because Magento's own parser reads them.
<?php
// app/code/Vendor/Loyalty/Test/Integration/Model/LegacyFixtureTest.php
declare(strict_types=1);
namespace Vendor\Loyalty\Test\Integration\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
class LegacyFixtureTest extends TestCase
{
/**
* @magentoDbIsolation enabled
* @magentoDataFixture Magento/Catalog/_files/product_simple.php
* @magentoConfigFixture current_store loyalty/points/per_unit 3
*/
public function testSimpleProductFixtureIsLoaded(): void
{
$product = Bootstrap::getObjectManager()
->get(ProductRepositoryInterface::class)
->get('simple');
self::assertSame('Simple Product', $product->getName());
}
}Pinning configuration and area for one test
#[Config] writes a configuration value for the duration of the test and restores it afterwards, so you never have to run bin/magento config:set to test a setting. #[AppArea] decides which area's di.xml and layout are loaded, which matters when the class you are testing is only wired in adminhtml. Both attributes can sit on the class, where they apply to every test in it, or on a single method.
<?php
// app/code/Vendor/Loyalty/Test/Integration/Model/AdminAreaTest.php
declare(strict_types=1);
namespace Vendor\Loyalty\Test\Integration\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\TestFramework\Fixture\AppArea;
use Magento\TestFramework\Fixture\Config;
use Magento\TestFramework\Fixture\DbIsolation;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use Vendor\Loyalty\Model\PointsCalculator;
#[AppArea('adminhtml')]
class AdminAreaTest extends TestCase
{
#[DbIsolation(true)]
#[Config('loyalty/points/per_unit', '3')]
public function testRateComesFromStoredConfiguration(): void
{
$calculator = Bootstrap::getObjectManager()->get(PointsCalculator::class);
self::assertSame(76, $calculator->award(25.40));
}
}
Discussion