Creating Fixtures for a Craft CMS Structure

Joshua Pease, Platform Development Technical Director

Article Category: #Code

Posted on

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.

Joshua Pease

Joshua is a Platform Development Technical Director in Walla Walla, Washington. He thrives on marrying great design and great development, creating stunning results.

More articles by Joshua

Related Articles