In Part 1 of this 2 parts article, we covered how to use data fixtures in integration and API functional test with real examples.
In this Part 2, we will cover how to create a data fixture.
You probably noticed this, data fixtures are just regular PHP classes that implement certain interfaces. These interfaces are DataFixtureInterface and RevertibleDataFixtureInterface.
Implement the interface RevertibleDataFixtureInterface if the data fixture data needs to be removed manually after the test is executed. For example product data or category data. You must manually remove this data.
In contrast, the interface DataFixtureInterface is intended for all cases where the data created is removed automatically when the related data is removed. For example AddShippingAddress adds a shipping address to existing cart data. This data fixture does not need to delete its data, because the data is automatically removed when the cart data is deleted from the database.
Data fixtures must implement the apply() method of the DataFixtureInterface interface and the revert() method of the RevertibleDataFixtureInterface interface (if applicable).
Before your test is executed, the application executes the apply() method of each data fixture configured in your test in the order that they are declared. This method is executed with the data that is passed in the second parameter of the DataFixture attribute. In this method, you generate data and return a reference to the data that is created or NULL if no data is created.
The revert() method is executed after the test has completed. The application executes the revert() method of each data fixture that was applied in reverse order. This method is executed with the data that was created and returned by the apply() method.
For example, let's create a data fixture that creates a CMS page.
class Page implements RevertibleDataFixtureInterface { public function __construct( private PageInterfaceFactory $pageFactory, private PageRepositoryInterface $pageRepository ) { } public function apply(array $data = []): ?DataObject { $page = $this->pageFactory->create(['data' => $data]); return $this->pageRepository->save($page); } public function revert(DataObject $data): void { $this->pageRepository->deleteById($data->getId()); } }
Simple, isn't it? Remember to always use APIs to create and delete fixtures if possible.
Now let's use this data fixture in a test.
Test Case 4
class PageViewTest extends AbstractController { /** * @dataProvider storeViewCodeDataProvider */ #[ DataFixture(StoreFixture::class, ['code' => 'french'], 'store2'), DataFixture(StoreFixture::class, ['code' => 'german'], 'store3'), DataFixture( PageFixture::class, ['identifier' => 'page1', 'title' => 'Gallery', 'content' => 'Next page', 'stores' => [1]], 'page1' ), DataFixture( PageFixture::class, ['identifier' => 'page1', 'title' => 'Galerie', 'content' => 'Page suivant', 'stores' => ['$store2.id$']], 'page2' ) ] public function testPageScope(string $storeViewCode, string $expectedText): void { $this->storeManager->setCurrentStore($storeViewCode); $fixtures = DataFixtureStorageManager::getStorage(); $page1 = $fixtures->get('page1'); $pageUrl = $page1->getIdentifier(); $this->dispatch("/$pageUrl"); $this->assertStringContainsString($expectedText, $this->getResponse()->getBody()); } public function pageScopeDataProvider(): array { return [ ['default', 'Next page'], ['french', 'Page suivant'], ['german', 'The page you requested was not found'], ]; } }
Perfect! We have created a data fixture and used it in a test with other fixtures. But wait, after reviewing this test one more time, I realized that we are setting the title even though we don't really use it in the test. So in this case it does not really matter what the page title is.
If you remember Test Case 3, we used \Magento\Catalog\Test\Fixture\Product data fixture and we only changed the price of the product as per requirements. So let's remove the title from the data.
Oops, it seems like it does not work. I don't know about you, but I received something like "Required field title is empty". So why didn't we have a similar issue with Test Case 1, where we did not pass any value into the second parameter? Well, remember I talked about data fixture default values? Our Magento\Cms\Test\Fixture\Page data fixture does not have default values. Let's add them!
class Page implements RevertibleDataFixtureInterface { private const DEFAULT_DATA = [ PageInterface::PAGE_ID => null, PageInterface::IDENTIFIER => null, PageInterface::TITLE => 'Page%uniqid%', PageInterface::PAGE_LAYOUT => '1column', PageInterface::META_TITLE => null, PageInterface::META_KEYWORDS => null, PageInterface::META_DESCRIPTION => null, PageInterface::CONTENT_HEADING => null, PageInterface::CONTENT => 'PageContent%uniqid%', PageInterface::CREATION_TIME => null, PageInterface::UPDATE_TIME => null, PageInterface::SORT_ORDER => 0, PageInterface::LAYOUT_UPDATE_XML => null, PageInterface::CUSTOM_THEME => null, PageInterface::CUSTOM_ROOT_TEMPLATE => null, PageInterface::CUSTOM_LAYOUT_UPDATE_XML => null, PageInterface::CUSTOM_THEME_FROM => null, PageInterface::CUSTOM_THEME_TO => null, PageInterface::IS_ACTIVE => true, // Not a part of the PageInterface 'stores' => [Store::DEFAULT_STORE_ID] ]; public function __construct( private PageInterfaceFactory $pageFactory, private PageRepositoryInterface $pageRepository, private ProcessorInterface $dataProcessor ) { } /** * {@inheritdoc} * @param array $data Parameters. Same format as Page::DEFAULT_DATA. */ public function apply(array $data = []): ?DataObject { $data = $this->dataProcessor->process($this, array_merge(self::DEFAULT_DATA, $data)); $page = $this->pageFactory->create(['data' => $data]); return $this->pageRepository->save($page); } /** * @inheritdoc */ public function revert(DataObject $data): void { $this->pageRepository->deleteById($data->getId()); } }
We have introduced a new constant in the page data fixture that holds the default values of the generated CMS page. The constant also serves as documentation of fields that can be configured. In order to make the default values unique, we used placeholder %uniqid% and data processor ProcessorInterface that substitutes the placeholder %uniqid% with a random string. Remember, this data processor must be explicitly executed if the default data uses placeholders. Now let's refactor our test!
class PageViewTest extends AbstractController { /** * @dataProvider storeViewCodeDataProvider */ #[ DataFixture(StoreFixture::class, ['code' => 'french'], 'store2'), DataFixture(StoreFixture::class, ['code' => 'german'], 'store3'), DataFixture( PageFixture::class, ['content' => 'Next page', 'stores' => [1]], 'page1' ), DataFixture( PageFixture::class, ['identifier' => '$page1.identifier$', 'content' => 'Page suivant', 'stores' => ['$store2.id$']], 'page2' ) ] public function testPageScope(string $storeViewCode, string $expectedText): void { $this->storeManager->setCurrentStore($storeViewCode); $fixtures = DataFixtureStorageManager::getStorage(); $page1 = $fixtures->get('page1'); $pageUrl = $page1->getIdentifier(); $this->dispatch("/$pageUrl"); $this->assertStringContainsString($expectedText, $this->getResponse()->getBody()); } public function pageScopeDataProvider(): array { return [ ['default', 'Next page'], ['french', 'Page suivant'], ['german', 'The page you requested was not found'], ]; } }
We removed the title from the data because it does not matter what value it contains. Additionally, we removed the identifier for page1 to let the system generate one based on the page title, and we assigned the page1 identifier to page2.
The default values must be chosen carefully as they impact directly the usage of the data fixture. The main rule is to choose a value that is less likely to be overridden. For instance, we don't want to use false as a default value for is_active, because (in most cases) we want to create an active page. The default values are meant to be simple and functional. For example, in our case, we can generate pages without having to configure any value as follows:
class PageRepositoryTest extends TestCase { #[ DataFixture(PageFixture::class, as: 'page1'), DataFixture(PageFixture::class, as: 'page2'), DataFixture(PageFixture::class, as: 'page3') ] public function testGetList(): void { $fixtures = DataFixtureStorageManager::getStorage(); $page1 = $fixtures->get('page1'); $page2 = $fixtures->get('page2'); $page3 = $fixtures->get('page3'); $searchCriteria = $this->pageSearchCriteriaFactory->create(); $list = $this->pageRepository->getList($searchCriteria); $identifiers = array_map(fn ($page) => $page->getIdentifier(), $list->getItems()); $this->assertContains($page1->getIdentifier(), $identifiers); $this->assertContains($page2->getIdentifier(), $identifiers); $this->assertContains($page3->getIdentifier(), $identifiers); } }
It is extremely important to make sure data is properly removed in order to reset the test environment to the state it was in before the test was executed. The reason for this is that tests are executed in groups (a.k.a., test suites), therefore it is critical that the next test result is not affected by the previous test leftovers. As an abstract example, consider a new test that becomes Test #1 in the queue, creates a product, performs certain assertions and completes with success. However, this test did not remove the product after execution. The existing Test #2 that is coming up next in line, also creates a product and tries to assert that there will be one product present in the catalog in the end. This test will fail because there will be two products in the catalog — one was created and not removed in the scope of the previous test, and another one created in the scope of the current test.
Sure enough, in real life there will be much more complicated cases, but the general idea is basically the same — data cleanup is important not just to maintain new tests, but also to preserve the integrity of existing tests.
It is also important to understand how to use Database Isolation and Application Isolation. You should familiarize yourself with those two concepts.
In this article, we have covered how to create a data fixture and how to use it in the test. Please refer to the documentation for more information.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.