On this page
Usage
For years, the most common way to provide test data for automated tests has been fixtures – hard-coded values, usually stored in text/yaml files.
But fixtures and the frameworks that rely on them have several drawbacks. They are not easily modifiable, which tends to lead to duplicate fixtures with unwieldy names like person-with-birthdate.yaml.
They are typically loaded "en masse" by test frameworks like Drupal's. This can be slow if many unnecessary fixtures are being loaded for each unit test. It also creates brittle sets of data. For example, if an automated test is searching for objects with a matching city name and expects to find one instance, but later a new fixture is added that also matches, the test will fail.
Test data factories solve these problems by making data and data loading more dynamic and programmable. A factory is written in code, and while it can simply mimic a fixture by supplying default values for an object, factory libraries offer many other useful features.
A note of caution: a one line factory invocation may hide a great deal of complexity and database integration. That may be fine for Kernel/Functional tests, but should be avoided for Unit tests (see the blog post Factories breed complexity). Prefer to use simpler, non-persisted objects in Unit tests.
Defining Factories
Creating factory definitions is fairly straightforward, Factory Lollipop use the Chain of Responsibility design pattern to loop through declared Factory and get the first most appropriate Factory to define your Factory.
Let's suppose we want to create a Factory for our Article Node with the following structure:
| Machine Name | Type | Default value |
|---|---|---|
| field_scheduled_at | Datetime | NULL |
| field_is_paid | Boolean | 0 |
The following example will illustrates the use of default values, fieldable entity, and associations.
We'll creating a new Factory resolver by implementing the \Drupal\factory_lollipop\FactoryInterface.
Here is the basic structure:
<?php
namespace Drupal\my_custom_module\Factories;
use Drupal\factory_lollipop\FactoryInterface;
use Drupal\factory_lollipop\FixtureFactory;
/**
* Creates Drupal Article Nodes Factory for use in tests.
*/
class NodeArticleFactory implements FactoryInterface {
/**
* {@inheritdoc}
*/
public function getName():string {
return 'my_project.definitions.node_article';
}
/**
* {@inheritdoc}
*/
public function resolve(FixtureFactory $lollipop): void {
// ...
}
}
In our example code, we named the Definition Factory Class my_project_node_article. This name must be unique across all your Factories, it will be used as Identifier of this resolver for Lazy-Loading, see the next part "Loading Factories". Therefore, we highly suggests to prefix the Definition Factory Class name with the module name, the project name to avoid confusion with the Factory Name itself (see below).
Now we can implement the resolve() method to define our Article Node Factory:
/**
* {@inheritdoc}
*/
public function resolve(FixtureFactory $lollipop): void {
// Define the node type "Article".
$lollipop->define('node type', 'node_type_article', [
'type' => 'article',
]);
// Add the "Scheduled at" field without default value.
$lollipop->define('entity field', 'node_article_field_scheduled_at', [
'entity_type' => 'node',
'name' => 'field_scheduled_at',
'bundle' => $lollipop->association('node_type_article'),
'type' => 'datetime',
]);
$lollipop->create('node_article_field_scheduled_at');
// Add the "Is Paid" field with a default value of False.
$lollipop->define('entity field', 'node_article_field_is_paid', [
'entity_type' => 'node',
'name' => 'field_is_paid',
'bundle' => $lollipop->association('node_type_article'),
'type' => 'boolean',
]);
$lollipop->create('node_article_field_is_paid');
// Define the Node Factory for Article".
$lollipop->define('node', 'node_article', [
'type' => $lollipop->association('node_type_article'),
// Setup the default Status to Published.
'status' => 1,
// Setup the "Is Paid" field default value to FALSE.
'field_is_paid' => FALSE,
]);
}In our example code, we defined 4 Factories:
node_type_article: The Node Type, necessary for Drupal;node_article_field_is_paid: The "Is paid" field attached to the Node Type Article;node_article_field_scheduled_at: The "Scheduled at" field attached to the Node Type Article;node_article: The Node itself, the one we will use for Data creation on the below "Using Factories".
Those 4 Factory Definition must be unique across all your Loaded Factories. As they will be used as Blueprint to generate Data.
With our Factory class complete, we're ready to add this service to our custom module's service file.
services:
my_custom_module.factories.node.article:
class: Drupal\my_custom_module\Factories\NodeArticleFactory
tags:
- { name: factory_lollipop.factory_resolver, priority: 1 }You may be interested by reading the Advanced Factory Definition for more in-real world use-case.
Loading Factories
Before reading those lines, please make sure you are familiar with Lollipop Factory tests integration.
It will be necessary to manually load factories in your Kernel/Functional test suite.
This can be accomplished by adding any necessary Definition Factory Class Name to the loader.
/**
* Example of Factory Lollipop usage for a KernelTest.
*/
class MyKernelTest extends LollipopKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
];
public function testCreateNode(): void {
$this->factoryLollipop->loadDefinitions(['my_project.definitions.node_article']);
}
}Using Factories
When using a factory, it is possible to override any of the options provided by the definition. Consequently, you always have control over the data at the time you create it. Using factories is as simple as:
public function testCreateNode(): void {
$this->factoryLollipop->loadDefinitions(['my_project.definitions.node_article']);
$node = $this->factoryLollipop->create('node_article', ['title' => 'Magna cursus tempor']);
}Drupal's property-access component is used to populate the properties, therefore I can override the title or any available field on my Node Article entity.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion