cancel
Showing results for 
Search instead for 
Did you mean: 

How to Use and Create Data Fixture in Integration and API Functional Tests (Part 1/2)

soumahadob
Senior Member

Introduction

 

In integration and API functional testing, you may need to change some default settings, and generate some data before you start checking the expected behavior. For example, if you would like to verify that a product URL key is not altered after updating the product name, you need to create a product, change the name of the product and save it before you check whether the URL key changed.

 

Test Case 1

  1. Create a product.
  2. Change the product name.
  3. Check that the product URL key did not change.

 

class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
    public function testUrlKeyShouldNotChangeAfterChangingProductName():void
    {
        $product = $this->productFactory->create();
        $product->setTypeId('simple')
            ->setAttributeSetId(4)
            ->setName('name')
            ->setSku('sku')
            ->setPrice(10);
        $this->productRepository->save($product);
        $urlKey = $product->getUrlKey();
        $product->setName('new name');
        $this->productRepository->save($product);
        $this->assertEquals($urlKey, $product->getUrlKey());
    }
}

In this implementation, we created a product, changed the name and checked whether the URL key changed. Now imagine that there are dozens of test cases that generate one to many products in different variations such as disabled, out of stock, invisible, assigned to a category and the list goes on. As a developer, you will start noticing redundancy and, you will certainly think of refactoring repetitive parts of your test code. For example, a product factory method or class to address the issue with duplicated codes. Well, that's what data fixtures are all about, except that they come with other advantages.

 

What is a data fixture?

 

A data fixture is a PHP script that creates and deletes data before and after the test is executed respectively.

Before we get into how to create a data fixture, let's begin with how to use it as you might not need to create one at all. There are dozens of these data fixtures in the application. For example, the data fixture \Magento\Catalog\Test\Fixture\Product creates a product.

 

How to use data fixtures

 

Data fixtures are declared using the PHP attribute \Magento\TestFramework\Fixture\DataFixture as follows:

 

#[
    DataFixture(ProductFixture::class)
]
class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
    public function testUrlKeyShouldNotChangeAfterChangingName(): void
    {
        //...
    }
}
class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
    #[
        DataFixture(ProductFixture::class)
    ]
    public function testUrlKeyShouldNotChangeAfterChangingName(): void
    {
        //...
    }
}

As shown in the preceding examples, you can declare the attribute at the test case class level or at the test method level. If declared at the test case class level, the data fixture is applied to each test in the test case class. If declared at the test case method level, the data fixture is applied only to the target test. If declared at both levels, then only data fixtures declared at the test method level are applied for that particular test.

The attribute \Magento\TestFramework\Fixture\DataFixture takes three parameters. The first parameter is required and must be the fully qualified name of the data fixture class. The second parameter (optional) are the values to be overridden or supplied to the data fixture. Only the specified values will be replaced while the other values remain set to their default values as defined in the data fixture. The third parameter (optional) is the fixture reference ID used to retrieve the data created by the data fixture.

For example, we can rewrite Test Case 1 as follows:

 

class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
    #[
        DataFixture(ProductFixture::class, ['sku' => 'simple-product'])
    ]
    public function testUrlKeyShouldNotChangeAfterChangingName(): void
    {
        $product = $this->productRepository->get('simple-product');
        $product->setName('new name');
        $this->productRepository->save($product);
        $this->assertEquals($urlKey, $product->getUrlKey());
    }
}

In this implementation, we used the existing product data fixture and, we got rid of the part of our test that created a product. The product is created before the test is executed and deleted after the test is executed.

Did you notice that we passed the SKU value in the second parameter? Well, we need the product model in the test and for that we need to retrieve the product created by the data fixture. To retrieve the product we need to know the SKU or the ID, since the default values are unpredictable. But there is a better way to get around this without having to override the random SKU. We can retrieve the data created by the fixture using the fixture alias as follows:

 

class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
    #[
        DataFixture(ProductFixture::class, as: 'product')
    ]
    public function testUrlKeyShouldNotChangeAfterChangingName(): void
    {
        $fixtures = DataFixtureStorageManager::getStorage();
        $product = $fixtures->get('product');
        $product = $this->productRepository->get($product->getSku());
        $product->setName('new name');
        $this->productRepository->save($product);
        $this->assertEquals($urlKey, $product->getUrlKey());
    }
}

In this implementation, we used Magento\TestFramework\Fixture\DataFixtureStorageManager to retrieve the data created by the data fixture by its alias. Still, we are retrieving the product model from the repository by SKU. This is very important because data fixtures are executed in a global scope, while the script inside the test method can be executed in a different scope using \Magento\TestFramework\Fixture\AppArea. To prevent unexpected behaviors, it is not recommended to modify this data or use it for assertions. Instead, retrieve a fresh instance of the model from the database.

 

Spoiler
Although you have the ability to override default values of the fixture, you should only override the default values when it is necessary.

The test is now completely refactored and compliant with best practices.

Now let's practice with a more advanced scenarios, shall we?

 

Test Case 2

  1. Create a non-anchor category and assign it to the default category.
  2. Create a second category and assign it to the other category.
  3. Create a product and assign it to the second category.
  4. Check that the product is present in the collection filtered by the second category.
  5. Check that the product is not present in the collection filtered by the first category.

 

class CollectionTest extends \PHPUnit\Framework\TestCase
{
    #[
        DataFixture(CategoryFixture::class, ['is_anchor' => 0], 'category1'),
        DataFixture(CategoryFixture::class, ['parent_id' => '$category1.id$'], 'category2'),
        DataFixture(ProductFixture::class, ['category_ids' => ['$category2.id$']], 'product')
    ]
    public function testAddCategoryFilter(): void
    {
        $fixtures = DataFixtureStorageManager::getStorage();
        $product = $fixtures->get('product');
        $category1 = $fixtures->get('category1');
        $category2 = $fixtures->get('category2');
        $collection = $this->collectionFactory->addCategoryFilter($category2);
        $this->assertEquals(1, $collection->getSize());
        $collection = $this->collectionFactory->addCategoryFilter($category1);
        $this->assertEquals(0, $collection->getSize());
    }
}

In this test, we implemented steps 1-3 with data fixtures. The first data fixture creates a category and sets is_anchor to 0 as per step#1 of the test scenario. The second data fixture creates a category and sets the parent_id to the ID of the category created in the preceding data fixture. The third data fixture creates a product and assigns it to the second category. You probably noticed the dollar signs around the values. Well, this is how a data fixture accesses data created in other data fixtures using their alias. The data can be accessed as whole $alias$ or a property $alias.prop_name$. In this case we are retrieving the ID of the categories created by the category's data fixtures $category1.id$ and $category2.id$.

 

You might have wondered, "how do I know the attribute names that need to be configured?". For example, in this test we configured is_anchor, parent_id and category_ids. The attribute names should be documented in the data fixture class. These attributes usually match the interface of the data model created by the data fixture. For example, the product data fixture accepts a similar data structure as the data model used to create a product via the API. But there are exceptions and these exceptions should be documented in the data fixture class. Make sure that you open the data fixture class and read the doc blocks (if necessary).

 

Test Case 3

  1. Create a simple product with price 100.
  2. Create a cart price rule with 50% coupon (discount should apply to shipping amount as well).
  3. Add a product to shopping cart.
  4. Add shipping address.
  5. Select flat rate shipping method.
  6. Check that the grand total is 105.
  7. Apply coupon.
  8. Check that the grand total is 52.5.

 

class CartPriceRuleTest extends \PHPUnit\Framework\TestCase
{
    #[
        DataFixture(ProductFixture::class, ['price' => 100], 'product'),
        DataFixture(ProductConditionFixture::class, ['attribute' => 'sku', 'value' => '$product.sku$'], 'condition'),
        DataFixture(
            CartPriceRuleFixture::class,
            [
                'coupon_code' => 'july4',
                'apply_to_shipping' => 1,
                'discount_amount' => 50,
                'actions' => ['$condition$']
            ],
        ),
        DataFixture(GuestCartFixture::class, as: 'cart'),
        DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
        DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
        DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$'])
    ]
    public function testAddCategoryFilter(): void
    {
        $fixtures = DataFixtureStorageManager::getStorage();
        $cart = $fixtures->get('cart');
        $cart = $this->cartRepository->get($cart->getId());
        $totals = $this->cartTotalsCollector->collect($cart);
        $this->assertEquals(105, $totals->getGrandTotal());
        $cart->setCouponCode('july4');
        $totals = $this->cartTotalsCollector->collect($cart);
        $this->assertEquals(52.5, $totals->getGrandTotal());
    }
}

In this test, we implemented steps 1-5 with data fixtures. Like in Test Case 2, we used fixture aliases to pass data from one fixture to another. Again, the overridden values are either required (e.g cart_id) or necessary as per the test steps.

You may deal with some functional areas that do not have data fixtures, and you will most likely have to create them. In the next article (Part 2/2 of this article), we will go through data fixture creation.

 

Conclusion

 

In this article, we have covered some basic concepts of using data fixtures in integration tests. This is the time for you to ask why didn't we cover API functional tests? Well, you will be surprised, when I say this! There is absolutely no difference in how you create or use data fixtures for integration tests and API tests. Both integration and API tests use the same data fixtures. Check out Part 2 to learn how to create a data fixture.

 

2 Comments