Working with data fixtures

Data fixtures are reusable components that set up test data in your database before running integration tests. They help you create a consistent, predictable test environment by populating the database with entities like products, customers, orders, categories, and more.

Think of data fixtures as the "arrange" phase of your test--they prepare the data your test needs to verify specific functionality.

Modern vs legacy approaches

Adobe Commerce and Magento Open Source support multiple ways to work with data fixtures, but not all are equally recommended.

data-variant=warning
data-slots=text
Legacy fixtures (file-based fixtures like Magento/Catalog/_files/product_simple.php) are deprecated. New legacy fixtures cannot be created. Use parameterized data fixtures instead for better maintainability, reusability, and type safety.
data-variant=success
data-slots=text
Parameterized data fixtures are the recommended approach. They require PHP attributes (#[DataFixture()]), which offer superior type safety, better IDE support, improved readability, and powerful parameterization capabilities. DocBlock annotations (@magentoDataFixture) only work with legacy file-based fixtures.

Comparison

Approach
Status
Can Parameterize
Syntax Example
Legacy fixtures (file-based)
Deprecated
❌ No
@magentoDataFixture Magento/Catalog/_files/product.php
Parameterized data fixtures (with PHP Attributes)
✅ Recommended
✅ Yes
#[DataFixture(ProductFixture::class, ['price' => 10], 'product')]
data-variant=warning
data-slots=text
Parameterized data fixtures cannot be used with DocBlock annotations (@magentoDataFixture). They require PHP Attributes (#[DataFixture()]).

Why avoid legacy fixtures?

Types of data fixtures

This section covers the two main types of data fixtures: legacy file-based fixtures and modern parameterized data fixtures.

These are PHP classes that implement DataFixtureInterface or RevertibleDataFixtureInterface. They support powerful parameterization and require PHP Attributes (#[DataFixture()]).

data-variant=warning
data-slots=text
Parameterized data fixtures cannot be used with DocBlock annotations. Only PHP Attributes support parameterization.

The power of modern parameterized data fixtures lies in their ability to be customized as you can configure the data they create without writing new fixture classes.

For detailed information on basic parameterization, aliases, store scope, and the count parameter, see DataFixture Attribute.

Basic example:

// Only override values that matter for your test. SKU is auto-generated with a unique value
#[
    DataFixture(ProductFixture::class, ['price' => 10.00], 'product')
]
public function testProductExists(): void
{
    $fixtures = DataFixtureStorageManager::getStorage();
    $product = $fixtures->get('product');
}

Fixture references - building complex scenarios:

You can reference data from one fixture in another using the $alias.property$ syntax. This allows you to build complex, interconnected test data scenarios:

#[
    DataFixture(CustomerFixture::class, as: 'customer'),
    DataFixture(User::class, as: 'user'),
    DataFixture(
        Company::class,
        ['sales_representative_id' => '$user.id$', 'super_user_id' => '$customer.id$'],
        'company'
    )
]

Legacy fixtures (Deprecated)

Legacy fixtures are file-based PHP scripts (stored in _files/ directories) that directly execute database operations. They cannot be parameterized and are now deprecated.

data-variant=warning
data-slots=text
New legacy fixtures cannot be created. Existing legacy fixtures should be migrated to parameterized data fixtures.

Example with DocBlock annotation:

/**
 * @magentoDataFixture Magento/Catalog/_files/product_simple.php
 */
public function testProductExists(): void
{
    // Test logic here - but you're stuck with whatever data the file creates
}

Example of a legacy fixture file: Magento/Catalog/_files/product_simple.php

<?php
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\TestFramework\Helper\Bootstrap;

$product = Bootstrap::getObjectManager()->create(Product::class);
$product->setTypeId('simple')
    ->setAttributeSetId(4)
    ->setName('Simple Product')
    ->setSku('simple')  // Fixed SKU - can't customize
    ->setPrice(10)      // Fixed price - can't customize
    ->setVisibility(4)
    ->setStatus(1);

Bootstrap::getObjectManager()
    ->get(ProductRepositoryInterface::class)
    ->save($product);

Problems: To create a product with a different SKU or price, you need to create an entirely new fixture file!

Migrating legacy fixtures to modern

If you have tests using legacy fixtures, you should migrate them to parameterized data fixtures with PHP Attributes. New legacy fixtures cannot be created.

Before (Legacy fixtures)

/**
 * @magentoDataFixture Magento/Catalog/_files/product_simple.php
 */
public function testProductPrice(): void
{
    $product = $this->productRepository->get('simple'); // Fixed SKU
    $this->assertEquals(10, $product->getPrice()); // Fixed price
}

After (Parameterized data fixtures with PHP Attributes)

#[
    DataFixture(ProductFixture::class, ['price' => 10], 'product')
]
public function testProductPrice(): void
{
    $fixtures = DataFixtureStorageManager::getStorage();
    $product = $fixtures->get('product');
    $this->assertEquals(10, $product->getPrice());
}

Retrieving fixture data

Always use DataFixtureStorageManager to retrieve fixture data created by fixtures:

use Magento\TestFramework\Fixture\DataFixtureStorageManager;

#[
    DataFixture(ProductFixture::class, ['price' => 25.00], 'test_product')
]
public function testProduct(): void
{
    $product = DataFixtureStorageManager::getStorage()->get('test_product');
    $this->assertEquals(25.00, $product->getPrice());
}

Complex example: Inventory with multiple sources

Example showing fixture composition and references:

use Magento\TestFramework\Fixture\DataFixtureStorageManager;

#[
    DataFixture(SourceFixture::class, ['source_code' => 'test_source'], 'source'),
    DataFixture(StockFixture::class, as: 'stock'),
    DataFixture(
        StockSourceLinksFixture::class,
        [['stock_id' => '$stock.stock_id$', 'source_code' => '$source.source_code$']]
    ),
    DataFixture(
        StockSalesChannelsFixture::class,
        ['stock_id' => '$stock.stock_id$', 'sales_channels' => ['base']]
    ),
    DataFixture(ProductFixture::class, as: 'p1'),
    DataFixture(ProductFixture::class, as: 'p2'),
    DataFixture(
        SourceItemsFixture::class,
        [
            ['sku' => '$p1.sku$', 'source_code' => '$source.source_code$', 'quantity' => 10, 'status' => 1],
            ['sku' => '$p2.sku$', 'source_code' => '$source.source_code$', 'quantity' => 20, 'status' => 1],
        ]
    ),
    DataFixture(Indexer::class, as: 'indexer')
]
public function testInventoryConfiguration(): void
{
    $stock = DataFixtureStorageManager::getStorage()->get('stock');
    $product1 = DataFixtureStorageManager::getStorage()->get('p1');
    // Test inventory logic
}

This example demonstrates:

Best practices

Follow these best practices when working with data fixtures.

Do not create legacy fixtures

data-variant=warning
data-slots=text
New legacy fixtures cannot be created. Do not create new tests using legacy fixtures (e.g., Magento/Catalog/_files/product_simple.php). Always use parameterized data fixtures instead. Existing legacy fixtures should be migrated.

Bad - Cannot create new:

/**
 * @magentoDataFixture Magento/Catalog/_files/product_simple.php
 */

Good - Use parameterized data fixtures with PHP Attributes:

#[DataFixture(ProductFixture::class, ['price' => 10])]
data-variant=info
data-slots=text
Only override fixture values that are necessary for your test. Unique fields like SKU are dynamically generated and should not be overridden unless specifically required by the test scenario.

Use PHP Attributes for parameterized data fixtures

Parameterized data fixtures require PHP Attributes.

Keep fixtures focused

Each fixture should do one thing. Use multiple fixtures and compose them rather than creating complex all-in-one fixtures.

Use fixture aliases for references

Always use the as parameter when you need to reference fixture data in other fixtures:

#[
    DataFixture(CategoryFixture::class, as: 'category'),
    DataFixture(ProductFixture::class, ['category_ids' => ['$category.id$']])
]

Leverage fixture defaults

Most fixtures have sensible defaults with dynamic unique values. Only override values that matter for your specific test scenario.

Don't override unique fields unnecessarily:

// Bad - overriding SKU when not needed
#[DataFixture(ProductFixture::class, ['sku' => 'my-sku', 'price' => 100], 'product')]

// Good - let SKU be auto-generated and reference it when needed
#[
    DataFixture(ProductFixture::class, ['price' => 100], 'product'),
    DataFixture(
        CatalogRuleFixture::class,
        [
            'is_active' => false,
            'conditions' => [['attribute' => 'sku', 'value' => '$product.sku$']]
        ],
        'rule'
    )
]
public function testCatalogRule(): void
{
    $product = DataFixtureStorageManager::getStorage()->get('product');
    $rule = DataFixtureStorageManager::getStorage()->get('rule');
    // Access auto-generated SKU
    $sku = $product->getSku();
}

You can reference auto-generated values using $alias.property$ syntax without overriding them.

Understand isolation behavior

data-variant=info
data-slots=text
Database isolation is enabled by default when using data fixtures. Application isolation has significant performance implications and should only be used when necessary (e.g., when modifying application state like sessions). See AppIsolation and DbIsolation for details.

Fixture cleanup is automatic

Fixtures that implement RevertibleDataFixtureInterface are automatically cleaned up. This is critical to prevent test pollution where data from one test affects another test.

data-variant=warning
data-slots=text
Proper data cleanup is essential. Tests run in groups (test suites), and leftover data from one test can cause unexpected failures in subsequent tests. Always ensure your fixtures properly implement the revert() method.

Rely only on explicitly configured fixtures

Your tests should depend on fixtures created before the test runs. However, only rely on things you've explicitly configured in your test's fixture declarations, not on implicit side effects or fixtures from other tests.

Finding available fixtures

Most Commerce modules include test fixtures in their Test/Fixture directory:

Check these directories in the Adobe Commerce or Magento Open Source codebase to discover available fixtures and their parameters.

Creating custom fixtures

If you need custom fixtures, create a class that implements DataFixtureInterface or RevertibleDataFixtureInterface.

data-variant=info
data-slots=text
For detailed guidance on creating custom data fixtures, see the official Magento DevBlog article: How to Create Data Fixtures (Part 2/2).

Basic structure

<?php
namespace Vendor\Module\Test\Fixture;

use Magento\TestFramework\Fixture\DataFixtureInterface;
use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface;

class CustomFixture implements RevertibleDataFixtureInterface
{
    public function __construct(
        private YourDependency $dependency
    ) {}

    public function apply(array $data = []): ?DataFixtureStorageInterface
    {
        // Create your entity
        $entity = $this->dependency->create($data);
        
        return new DataFixtureStorage($entity);
    }
    
    public function revert(DataFixtureInterface $dataFixture): void
    {
        // Clean up the entity
        $entity = $dataFixture->get();
        $this->dependency->delete($entity);
    }
}

When to use RevertibleDataFixtureInterface

Use RevertibleDataFixtureInterface when the data fixture creates data that needs to be manually removed after the test. Examples:

Use DataFixtureInterface when the data is automatically removed with related data. Examples:

Learn more

Refer to the following resources for more information on data fixtures.

Official Documentation

DevBlog Articles