Creating Fixtures for a Craft CMS Structure
Structure entries can be tricky to set up for Craft CMS unit tests. Let’s learn how to make structures and fixtures come together in perfect harmony.
Want to try this out on a real Craft site? This article has its own GitHub repo. Follow along and create your own fixtures and tests.
https://github.com/joshuapease/craft-unit-testing-playground
This is the article I wish I'd found when I was thrashing around Google and searching "How to create fixtures for a Craft CMS Structure".
For a recent project, I was hoping to unit test a helper function that creates breadcrumbs from an entry’s ancestors. But I got stuck trying to add that structure data as an Entry Fixture.
Here's the solution I came to, as well as tips that may help if you get stuck setting up fixtures in Craft.
What are fixtures?
You can think of fixtures as a way to provide a fixed starting point for your Craft project’s database. If you’re new to concept or how they work with Craft CMS, I’d recommend reading Craft’s testing docs and exploring how they’re set up in the Craft Github repo.
When your tests are configured to use an existing project.yaml
file, you don't need to create fixtures for things like fields, section configs, entry types, asset volumes and the like. However, you’ll still need to manually create fixtures for the actual content of your sections.
This article shares how to define a nested structure as a Craft fixture.
Overview
In order to load fixture entries in the right order, we need to use the $depends
property. Each descendent will also need its newParentId
set to its corresponding parent entry.
By the end of the post, we’ll have three structure entries nested like this.
Create your fixtures
For a three-level structure, you need to make three separate fixture classes.
I've named mine StructureL3Fixture
, StructureL2Fixture
and StructureL1Fixture
.
We use the $depends
property to make sure that each parent entry in the tree is loaded before its child.
StructureL3Fixture
depends on StructureL2Fixture
, which depends on StructureL1Fixture
.
Here's what your final fixtures
directory will look like once you've set everything up.
tests
└── fixtures
├── StructureL1Fixture.php
├── StructureL2Fixture.php
├── StructureL3Fixture.php
└── data
├── structure-l1.php
├── structure-l2.php
└── structure-l3.php
Let's start by setting up the top-most entry in our structure and work our way down the tree.
StructureL1Fixture.php
The L1
entry has no parent and doesn't depend on any other fixtures. This would be the level 1 entry in your structure.
/**
* 📁 file: tests/fixtures/StructureL1Fixture.php
*/
namespace craftunittests\fixtures;
use craft\test\fixtures\elements\EntryFixture;
class StructureL1Fixture extends EntryFixture
{
/**
* @inheritdoc
*/
public $dataFile = __DIR__ . '/data/structure-l1.php';
}
/**
* 📁 file: tests/fixtures/data/structure-l1.php
*/
$section = Craft::$app->sections->getSectionByHandle('testStructure');
return [
[
'authorId' => '1',
'sectionId' => $section->id,
'typeId' => $section->entryTypes[0]->id,
'slug' => 'structure-l1',
'title' => 'L1',
],
];
StructureL2Fixture.php
The level two entry is created in StructureL2Fixture
. It depends on StructureL1Fixture
.
/**
* 📁 file: tests/fixtures/StructureL2Fixture.php
*/
namespace craftunittests\fixtures;
use craft\test\fixtures\elements\EntryFixture;
class StructureL2Fixture extends EntryFixture
{
/**
* @inheritdoc
*/
public $dataFile = __DIR__ . '/data/structure-l2.php';
/**
* @inheritdoc
*/
public $depends = [
// ✨ This ensures that the parent entry is created first
StructureL1Fixture::class
];
}
/**
* 📁 file: tests/fixtures/data/structure-l2.php
*/
use craft\elements\Entry;
$section = Craft::$app->sections->getSectionByHandle('testStructure');
// ✨ This is where slugs come in handy
$parent = Entry::findOne(['slug' => 'structure-l1']);
return [
[
'authorId' => '1',
'sectionId' => $section->id,
'typeId' => $section->entryTypes[0]->id,
'slug' => 'structure-l2',
'title' => 'L2',
'newParentId' => $parent->id,
],
];
StructureL3Fixture.php
StructureL3Fixture
creates your level three entry. Because we’ve set up $depends
in our fixtures, all of its ancestor entries will automatically be created.
// 📁 file: tests/fixtures/StructureL3Fixture.php
<?php
namespace craftunittests\fixtures;
use craft\test\fixtures\elements\EntryFixture;
class StructureL3Fixture extends EntryFixture
{
/**
* @inheritdoc
*/
public $dataFile = __DIR__ . '/data/structure.php';
/**
* @inheritdoc
*/
public $depends = [
// ✨ This ensures that the parent enetry is created first
StructureL2Fixture::class
];
}
// 📁 file: tests/fixtures/data/structure.php
<?php
use craft\elements\Entry;
$section = Craft::$app->sections->getSectionByHandle('testStructure');
$parent = Entry::findOne(['slug' => 'structure-l2']);
return [
[
'authorId' => '1',
'sectionId' => $section->id,
'typeId' => $section->entryTypes[0]->id,
'slug' => 'structure-l3',
'title' => 'L3',
'newParentId' => $parent->id,
],
];
Your three-level structure is now complete. You can query structure-l3
and write all sorts of tests centered around its parent
and ancestors
attributes. Or, you can do the opposite and query structure-l1
and make use of children
and descendants
.
Test time!
Now it's time to have some fun! Or at least have more fun than setting up fixtures. Once you add the StructureL3Fixture::class
to your test's _fixtures()
function, you can start making queries like this.
$entry = Entry::findOne([
'slug' => 'structure-l3',
]);
This unit text example is for a Nav service that will return an array of breadcrumbs for a structure entry.
namespace craftunittests\unit;
use Codeception\Test\Unit;
use UnitTester;
use craft\elements\Entry;
use craft\helpers\App;
use modules\craftunit\services\Nav;
use craftunittests\fixtures\StructureL3Fixture;
class GetBreadcrumbsFromAncestors extends Unit
{
/**
* @var UnitTester
*/
protected $tester;
public function _fixtures(): array
{
return [
'entries' => [
'class' => StructureL3Fixture::class,
],
];
}
public function testGetBreadcrumbsFromAncestors()
{
$primaryUrl = App::env('PRIMARY_SITE_URL');
// 🔍 Query by slug to find structure-l3
$entry = Entry::findOne([
'slug' => 'structure-l3',
]);
$breadcrumbs = Nav::getBreadcrumbsFromAncestors($entry);
$this->assertEquals([
[
'title' => 'L1',
'url' => "{$primaryUrl}/test-structure/structure-l1",
],
[
'title' => 'L2',
'url' => "{$primaryUrl}/test-structure/structure-l1/structure-l2",
],
], $breadcrumbs);
}
}
Getting unstuck
I got majorly stuck trying to create relationships using the entry's parent
attribute. It just doesn’t seem to work with fixtures.
After digging through Craft’s source code, I discovered that EntriesController
uses $newParentId
to modify Structure entries.
Sadly, $newParentId
is marked as an @internal
property and doesn’t appear in the Craft Class Reference.
Because fixtures add an extra layer of abstraction to creating entries, it's easy to feel lost at times. But, behind the scenes we’re still calling the same Craft API that a plugin or module would use. Any knowledge gained learning how to create and edit entries with PHP gives you a leg up when it comes to testing.
If you’d like to play with these fixtures and tests in a real working project, check out the Github Repo. I plan to keep adding more examples and concepts related to unit testing Craft CMS.