Drupal 10: Using Default Content Deploy To Create Testing Content

Performing behavioural testing on a Drupal project allows you to make sure that all of your features work as they should do. This is especially important when you update code as this can introduce bugs that are otherwise time consuming or difficult to spot.

One aspect that is important to behavioural tests is the content of the site, which is often integral to many Drupal sites functioning correctly. Many Drupal sites have taxonomy terms that are used in views to filter content in one way or another.

There are also structure pages that are used as signposts to other parts of the site, and they are often important in navigation. Whilst you could just visit the pages directly, it's often useful to test the user journey end-to-end, which involves being able to navigate to the functionality being tested.

One approach to ensuring the site contains content is to copy the production database into the testing environment. Using the production database for testing has several disadvantages, not least of which is the complexity of copying the data across in the first place. Some production databases are very large and so importing this into the test environment can cause tests to take many hours to complete.

The biggest problem, though, is making sure that your development site doesn't contain any personal information as this is a security concern and can even cause problems like sending test emails to users. Whilst there are ways around this, teams can often spend quite a lot of time ensuring that there is no personally identifiable information at rest in your test environment.

A better approach to this is to use default content to create a Drupal site so that is in a known state before tests are run. This means that the site will function in the same way as the production site, just without all of the personal information. The idea behind this approach is that you create a known test environment for tests to run.

This uses the methodology called Arrange, Act, Assert (also known as AAA) where the site is set up in known state, actions are run on that content, and then tests are performed to ensure that the correct functionality was present.

To get this system working in Drupal I use the Default Content Deploy module, which allows both the export and import of content into a site. This module exports content as a series of JSON files, which can then be imported (or re-imported) when required.

In this article I will look at how to get up and running with Default Content Deploy and what workflow you can use to make life easy with this module.

Installing And Configuring Default Content Deploy

To install the module you first need to add it to the source code of your site using composer.

composer require --dev drupal/default_content_deploy

The --dev flag is used to ensure that the module doesn't reach your production environment.

The module can then be activated using the following Drush command.

drush en default_content_deploy

Once installed, you just need to add a configuration setting to your settings.php file to let the module know where to export the content to. The following will set the content export directory to a directory called "content", outside of the Drupal web root but you can also set this to an absolute path if required.

$settings['default_content_deploy_content_directory'] = '../content';

You can check that the module is installed correctly by running Drush and looking for the following output.

default-content-deploy:                                                                                                                                  
  default-content-deploy:entity-list (dcd-entity-list)             List current content entity types.                                                    
  default-content-deploy:export (dcde)                             Exports a single entity or group of entities.                                         
  default-content-deploy:export-site (dcdes)                       Exports a whole site content.                                                         
  default-content-deploy:export-with-references (dcder)            Exports a single entity with references.                                              
  default-content-deploy:import (dcdi)                             Import all the content defined in a content directory.                                
  default-content-deploy:uuid-info (dcd-uuid-info, dcd-uuid)       Get UUID of entity.

You are now ready to start exporting content.

Exporting Content Using Default Content Deploy

To export you need to know what you want to export. Use the default-content-deploy:entity-list Drush command.

drush default-content-deploy:entity-list

This will print a list of all of the content you can export, which in the case on a standard Drupal install is the following.

block_content (Custom block)
comment (Comment)
contact_message (Contact message)
file (File)
menu_link_content (Custom menu link)
node (Content)
paragraph (Paragraph)
path_alias (URL alias)
shortcut (Shortcut link)
taxonomy_term (Taxonomy term)
user (User)

You can export the entire site using the default-content-deploy:export-site Drush command.

drush default-content-deploy:export-site

This action will create a bunch of JSON files in the drupal/content directory, which is where we previously set the export destination.

However, exporting the entire site can often lead to problems as you will export every type of entity that has content on the site. This includes items like menu items and path aliases, which aren't needed in the export as they are generated automatically when content is created. You'll also export the default anonymous and administration users, which are generated when the site is installed. This can lead to problems when trying to import your content or updating your content export later.

A better plan is to pick and chose the content you want to export. It's best to selected the pages of content you want to export first and the branch out from there. For example, you can export all nodes in your site by using the default-content-deploy:export command and passing the type of "node":

drush default-content-deploy:export node

It is also possible to supply parameters to single out particular pages or types of page as a single item or a comma separated list.

Here are some examples of this command in action.

# Export page 1.
drush default-content-deploy:export node --entity_id=1

# Export pages 1 and 2.
drush default-content-deploy:export node --entity_id=1,2

# Export only nodes of type "page".
drush default-content-deploy:export node --bundle=page

# Export nodes of type "page" and "article".
drush default-content-deploy:export node --bundle=page,article

# Export all nodes except page 3.
drush default-content-deploy:export node --skip_entities=3

The Default Content Deploy module doesn't export referenced entities, so once you have your pages exported you'll then need to export all the content that is connected to that entity. For example, the following entity types are normally connected to nodes and should be exported once you have the nodes you need.

Export all taxonomy terms.

drush default-content-deploy:export taxonomy_term

Export all media items.

drush default-content-deploy:export media

Export all files.

drush default-content-deploy:export file

Export all paragraphs.

drush default-content-deploy:export paragraph

You must export all of the content that you want in order to generate the full site once imported again, which might involve finding out the IDs of the different entities that you want to export. Default Content Deploy does have a Drush command called default-content-deploy:export-with-references, which should simplify this process, but I haven't managed to get this working.

Some notes on exporting content.

  • It is a good idea to fully re-install the site and then export the content changes that you need. This is much more preferable than attempting to export content from a site that contains other content items.
  • Vocabularies are not exported by default content as they are part of the configuration system. You still need to export any terms that you want to have available.
  • The Smart Date module field has some problems with the export, so you'll need to fix any Event bundle exports you create. (See common problems below).

Importing Content

To import all content from the content directory run the following Drush command.

drush default-content-deploy:import --force-override --yes

This command will tell you the number of entities created and report a success when complete. The force-override will force any already imported content to be updated with the exported version. If this is the first time you are installing the content then you can leave this parameter off if you like.

You can add the verbose flag to print out more information about what was imported.

Exporting Files And Media Into Test Content

Default files are handled by the Better Normalizers module, you can add this module to your codebase with the following require command.

composer require --dev drupal/better_normalizers

Then enable it with the following drush command.

drush en better_normalizers

With this in place you can now export file entities without any problems. The file itself will be base64 encoded into the content json.

Note that this module also requires that you set "minimum-stability" to "beta" in your composer.json file. There isn't a Drupal 10 version just yet, but work is being done to solve that.

Default Content Deploy Workflow

The import and export process have been looked at separately, but how can we tie these processes together to achieve the greatest success when creating new test content?

The following steps will ensure maximum success when using this system.

1) Re-install the site and then enable the default_content_deploy and better_normalizers modules. Due to the relationships between content items and their ID numbers in the system you are best off starting from scratch to avoid any confusion about what content items you are exporting.

2) Import everything to ensure all of the content in the site is “default”.

3) Make the content changes you need, whilst making sure to note the types of content you are adding.

4) Export the content changes by specifically targeting the content entities you updated. The Default Content Deploy module doesn’t “drill down” into content to export references, so you will need to ensure that you export any linked entities you also want to be imported as default content. This basically means that you'll want to export the files, export the media items, export the paragraphs, then export the nodes in that order. Best results are normally obtained by just exporting all of one type of entity at a time, rather than selecting paragraph IDs or bundle types.

5) Test the import! Once you have exported everything then re-install the site and test the import process.

6) Ensure that your test suite runs. You are exporting content in order to generate tests against that content, so you need to make sure that your test suite runs with the changes you've made to the content export.

7) Write your new tests.

8) If everything works, commit your files to git.

Tips:

- It’s a really good idea no to change too much at once. Add a single page or a paragraph at a time and ensure that everything works together. These changes are much easier to spot in git as well.
- Don’t be surprised to see already exported files changing. Things like ID numbers and other ancillary information might update as you import and export content. This is normal.
- Make sure you export both parts of an entity you want to update. This means exporting the nodes AND the referenced entities like paragraphs or taxonomy terms.
- As default content is used for testing to not be afraid of exporting more than one node that will be acted upon by a test. You might, for example, want to test comments on one node and publishing workflows on another node. This keeps your tests clean and prevents them from dependent on each other and therefore being brittle.

Common Problems

Here is a some comment problems you might come across when using the Content Deploy Module and their solutions.

Exporting Everything Causes Problems

Whilst the option to export every item of content from a site is there, doing so will often lead to errors. These errors are mainly caused by modules that aren't compatible with the content export process and so cause errors when trying to import them. You will also find that quite a few items of content are generated when a site is installed and so these items will be exported along with your actual testing content. Things like menu items, paths, and even the default anonymous user will be exported in this content.

It's a much better approach to export just the content you need for the test

Exporting Comments Doesn't Allow Them To Be Imported

Exporting comments using this module is quite error prone. This isn't a problem with the module, but more a problem with ensuring the import process links to the correct item of content and correct user, which can often lead to comments with broken connections to non-existent users.

If you absolutely have to use comments in your tests then it is probably better to use a behavioural testing framework to create the comment against the default content and then do whatever you need to do. I have found this far easier than attempting to get comments importing correctly.

Entity Type Does Not Support UUIDs

This happens when you attempt to export an entity that doesn't use UUIDs, which can happen if you have created a custom entity type that doesn't use this field. There are also a number of third party modules that probably don't support UUIDs and so won't be supported by the Default Content Deploy module.

This problem is easy to spot in the export process as it will create files called ".json" in your content export directory.

content/
-- my_custom_entity/
---- .json

The solution to this problem is to alter the entity so that it supports UUIDs, or use another mechanism (like install profiles) to import them into your site.

Field "value" Is Unknown

This is caused by certain field types that do not export correctly to the default content json format. One example of this is the Smart Date field on Event content items.

The problem is caused because the field isn’t wrapped in a field definition. Instead, the contents of the field are dumped directly to the root of the json.

For example, here is an event node exported from the site:

"field_event_capacity": [
    {
      "value": 100
    }
  ],
  "value": "2030-01-01T09:00:00+00:00",
  "end_value": "2030-01-01T10:00:00+00:00",
  "duration": 60,
  "rrule": null,
  "rrule_index": null,
  "timezone": "",
  "format": "Y-m-d\\TH:i:sP"
  "field_event_location": [
    {
      "value": "Online"
    }
  ],

To fix this, we need to wrap the event date field in a field definition:

"field_event_capacity": [
    {
      "value": 100
    }
  ],
  "field_event_date": [
    {
      "value": "2030-01-01T09:00:00+00:00",
      "end_value": "2030-01-01T10:00:00+00:00",
      "duration": 60,
      "rrule": null,
      "rrule_index": null,
      "timezone": "",
      "format": "Y-m-d\\TH:i:sP"
    }
  ],
  "field_event_location": [
    {
      "value": "Online"
    }
  ],

The content will now import correctly.

Adding To Your Testing Workflow

Now that you have a good handle on your default content you need to think about your testing workflow. The default content you create, whilst useful for local development, really excels when coupled with a decent behavioural testing system. All you need to to is setup the Drupal site in the correct way in order to perform the tests you need. How you do this depends on the system you are running, but you'll probably be starting from scratch, which is the case on many continuous integration environments.

How you set up your environment will depend on what you have available in your continuous integration environment, but you'll need access to a database of some kind and the ability to install and use Drupal through a web address. I have found that using docker containers really helps with this as it simplifies the complexity of the environment setup. You just offset all the versions and complexity to your docker container setup instead.

Once the environment is ready, the first thing to do is to get your site installed. This can be done with a few Drush commands.

# Install the site.
drush si standard --existing-config --yes --account-name=admin --account-pass=admin
# Clear the Drupal cache.
drush cr
# Import the site config.
drush cim -y

You should also remember to run any theme builders at this point so that the site will run with the fully built theme. This step depends on the system you used to build the theme, but it will probably involve the following commands.

cd web/themes/custom/my_site_theme
npm install
npm run build

Once that's complete you can then install the modules needed to import the content, namely the Default Content Deploy and the Better Normalizers modules.

drush en default_content_deploy better_normalizers --yes

The reason that these modules are enabled separately is to reduce the complexity around the site configuration. These modules are only used to test the site with content and so will only ever be installed when that task is needed. So, a secondary command that quickly installs the modules before the tests begin is a simple step and can be baked into any testing setup required.

Once the modules are installed you can import your default content using the following command.

drush default-content-deploy:import --force-override --yes

With the site setup, the modules installed, and your content imported, you are ready to start testing your site. This means navigating around the site and using it just like a normal user would, but in this case the site is in a known state and so our tests are more predictable.

In an ideal world you would fully re-install Drupal every time you run a new test, however, this might not always be the best solution due to the time taken to perform the install process. The compromise is to re-import the content at the start of every test run. There is a balance between ensuring a pure environment to run your tests versus the time taken for the tests to complete.

You do need to be careful when writing your tests so that they do not overlap functionality that might not be reset when the content is re-imported. Some entities are not effected by the content import process and so will left behind in their original state. This means that testing for things like shopping carts, likes, comments, or other ancillary page data you sometimes need new strategies to test correctly. You might need to generate different users or different pages of content in order to perform some of your tests independently.

As an example of this setup, let's look at Cypress. If you are using Cypress to run your behavioural tests then you can run the default-content-deploy:import command to re-import the content at the start of every test run. This can be achieved in Cypress using the before() function, which lives in the cypress/support/e2e.js file in your Cypress directory.

before(function () {
    // Import the default content every time we run a test.
    cy.exec('./vendor/bin/drush default-content-deploy:import --force-override --yes',{ timeout: 10000 }).its('code').should('eq', 0)
});

This command essentially run the Drush command to import the config, and will ensure that a return status of 0 is returned, which means the command had no errors. Note that the timeout is set quite high here, this is to allow enough time for all of your content models to be imported and updated correctly and allows for some future expansion of the test suite.

You can use the after() function (i.e. the opposite of the before() function) within your test suites in order to put the site back into a known state. This is especially important if you have created ancillary page data that might cause problems with other tests. Here is an example template of a test suite that you would add to Cypress.

describe('An example test suite', () => {

  after(function () {
    // Delete any data the test created.
  })

  it('passes', () => {
    // Run test.
  })
})

Rather than searching for and deleting the content you created in Cypress you instead want to create a Drush command that clears out the data.

Conclusion

The Default Content Deploy module is a really powerful module, and augmenting it with the Better Normalizers module makes it even more useful when testing content. It also makes life easier for developers on the project as they don't need to rely on copies of the production database to get work done. Instead, they can just import the default content and get started doing the work they need to do.

There are some niggles with the module though. Some types of entity aren't really suitable to be exported so you should try to avoid them if possible. Plus, not all modules will be compatible with the JSON format required for Default Content Deploy and so you might need to spend time fixing those issues.

You should watch out for dependencies in your content as although it is possible to export both paragraphs and nodes it's not easy to spot what entity is connected to what without importing them.

Instead of getting the test environment or developers to run individual Drush commands you should instead wrap those commands somehow. This can be through a Makefile or a task running or some kind, but you should avoid having to remember what flags to add when importing (or exporting) your content.

Be careful when writing your tests using this technique as you can quickly create testing mistakes that make life difficult for the team. Make use of the "before" and "after" actions in your test suite to ensure that the environment is in a known state. Take a look at my article on 7 common mistakes to avoid when writing tests to ensure that you aren't making life difficult for yourself in the long run.

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
2 + 2 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.