oliverdavies.uk/source/_pages/atdc/8.md
2024-02-18 01:35:59 +00:00

10 KiB

title permalink
ATDC: Lesson 8 - Tagging posts and test configuration /atdc/8-tagging-posts-test-configuration

{% block head_meta %}

{% endblock %}

{% block content %} In this lesson, let's add tags to our posts using the PostBuilder.

As we're doing test-driven development, start by creating a new PostBuilderTest:

<?php

// web/modules/custom/atdc/tests/src/Kernel/Builder/PostBuilderTest.php

namespace Drupal\Tests\atdc\Kernel\Builder;

use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;

final class PostBuilderTest extends EntityKernelTestBase {
}

As it's a Kernel test for a Builder class, place it within a Kernel/Buider directory and the equivalent namespace.

Testing the PostBuilder

Instead of writing test methods starting with test, you can also use an @test annotation or, in more recent versions of PHPUnit, a #[Test] attribute. This, with snake-case method names, is a popular approach as it can be easier to read.

Just be aware that if you write tests this way and don't add the annotation or attribute, the test won't be executed.

Let's start by testing the existing functionality within PostBuilder by verifying it returns published and unpublished posts.

Create these tests, which should pass by default as the code is already written:

/** @test */
public function it_returns_a_published_post(): void {
  $node = PostBuilder::create()
    ->setTitle('test')
    ->isPublished()
    ->getPost();

  self::assertInstanceOf(NodeInterface::class, $node);
  self::assertSame('post', $node->bundle());
  self::assertTrue($node->isPublished());
}

/** @test */
public function it_returns_an_unpublished_post(): void {
  $node = PostBuilder::create()
    ->setTitle('test')
    ->isNotPublished()
    ->getPost();

  self::assertInstanceOf(NodeInterface::class, $node);
  self::assertSame('post', $node->bundle());
  self::assertFalse($node->isPublished());
}

In both tests, we create a new post node with PostBuilder, set a title and the appropriate status, get the post and assert it's in the correct state.

To verify the tests are working correctly, try changing some values in it and PostBuilder to see if it fails when expected.

Tagging posts

Next, create a test for adding tags to a post.

It should be mostly the same as the others, but instead of an assertion for the published status, try to use var_dump() to see the value of field_tags:

/** @test */
public function it_returns_a_post_with_tags(): void {
  $node = PostBuilder::create()
    ->setTitle('test')
    ->getPost();

  self::assertInstanceOf(NodeInterface::class, $node);
  self::assertSame('post', $node->bundle());
  var_dump($node->get('field_tags'));
}

You should see an error message like this:

InvalidArgumentException: Field field_tags is unknown.

Why is this if field_tags is a default field that's created when Drupal is installed?

The same as users and content, the new Drupal instance is created for each test, which won't have your existing fields, so, in the test, you need to install the required configuration.

Creating a test module

To have the required configuration available, it can be added to the atdc module.

Any configuration files within a config/install directory can be installed, although if a site already has field_tags defined, we don't want to cause a conflict.

The convention is to create a test module that will only be required within the appropriate tests and to place the configuration there.

To do this, create a web/modules/custom/atdc/modules/atdc_test directory and an atdc_test.info.yml file with this content:

name: ATDC Test
type: module
core_version_requirement: ^10
hidden: true

Because of adding hidden: true, it won't appear in the modules list in Drupal's admin UI and avoid it being installed outside of the test environment.

Within the module, create a config/install directory, which is where the configuration files will be placed.

Creating configuration

But how do you know what to name the configuration files and what content to put in them?

Rather than trying to write them by hand, I create the configuration I need, such as fields, within a Drupal site and then export and edit the files I need.

To do that for this project, as I'm using the PHP built-in web server, I can use Drush to install Drupal using an SQLite database:

./vendor/bin/drush site:install --db-url sqlite://localhost/atdc.sqlite

Note: if you need Drush, run composer require drush/drush to install it via Composer.

If you have a database available, you can use that, too.

Once Drupal is installed and the configuration has been created, you can go to - /admin/config/development/configuration/single/export and select the configuration type and name.

The filename is shown at the bottom of the page, and you can copy the content into files within your module.

The uuid and _core values are site-specific, so they can be removed.

To add field_tags, you'll need both the field and field storage configuration.

These are the files that I created in my module based on the field I created.

field.field.node.post.field_tags.yml:

langcode: en
status: true
dependencies:
  config:
    - field.storage.node.field_tags
    - node.type.post
    - taxonomy.vocabulary.tags
id: node.post.field_tags
field_name: field_tags
entity_type: node
bundle: post
label: Tags
description: 'Enter a comma-separated list. For example: Amsterdam, Mexico City, "Cleveland, Ohio"'
required: false
translatable: true
default_value: {  }
default_value_callback: ''
settings:
  handler: 'default:taxonomy_term'
  handler_settings:
    target_bundles:
      tags: tags
    sort:
      field: _none
    auto_create: true
field_type: entity_reference

field.storage.node.field_tags.yml:

langcode: en
status: true
dependencies:
  module:
    - node
    - taxonomy
id: node.field_tags
field_name: field_tags
entity_type: node
type: entity_reference
settings:
  target_type: taxonomy_term
module: core
locked: false
cardinality: -1
translatable: true
indexes: {  }
persist_with_no_fields: false
custom_storage: false

Then, enable the module within PostBuilderTest:

protected static $modules = [
  // Core.
  'node',

  // Custom.
  'atdc_test',
];

Finally, install the configuration to create the field. Add this within the test:

$this->installConfig(modules: [
  'atdc_test',
]);

After adding this and attempting to install the configuration to add the field, you'll get an error:

Exception when installing config for module atdc_test, the message was: Field 'field_tags' on entity type 'node' references a target entity type 'taxonomy_term', which does not exist.

Fixing setup issues

The test is trying to install the field_tags, but it's missing a dependency. Tags reference a taxonomy term, but we haven't enabled the Taxonomy module within the test.

Enable the taxonomy module by adding it to the $modules array, and the error should change:

Exception when installing config for module atdc_test, message was: Missing bundle entity, entity type node_type, entity id post.

As well as the field configuration, we also need to create the Post content type.

This can be done by creating a node.type.post.yml file:

langcode: en
status: true
dependencies: {  }
name: Post
type: post
description: ''
help: ''
new_revision: true
preview_mode: 1
display_submitted: true

With this configuration, field_tags should be created on the Post content type, which is enough for the current test to pass.

Setting tags

Let's update the test and add assertions about the tags being saved and returned.

Get the tags from the post and assert that three tags are returned:

$tags = $node->get('field_tags')->referencedEntities();
self::assertCount(3, $tags);

As none have been added, this would fail the test.

Update the test to use a setTags() method that you haven't created yet:

$node = PostBuilder::create()
  ->setTitle('test')
  ->setTags(['Drupal', 'PHP', 'Testing'])
  ->getPost();

You should get an error confirming the method is undefined:

Error: Call to undefined method Drupal\atdc\Builder\PostBuilder::setTags()

To fix this, add the tags property and setTags() method to PostBuilder:

/**
 * @var string[]
 */
private array $tags = [];

/**
 * @param string[] $tags
 */
public function setTags(array $tags): self {
  $this->tags = $tags;

  return $this;
}

Tags will be an array of strings, and setTags() should set the tags to the tags property.

Next, add the logic to getPost() to create a taxonomy term for each tag name.

$tagTerms = [];

if ($this->tags !== []) {
  foreach ($this->tags as $tag) {
    $term = Term::create([
      'name' => $tag,
      'vid' => 'tags',
    ]);

    $term->save();

    $tagTerms[] = $term;
  }

  $post->set('field_tags', $tagTerms);
}

If $this->tags is not empty, create a new taxonomy term for each one and save it to the post.

Adding tag assertions

As well as asserting we have the correct number of tags, let's also assert that the correct tag names are returned and that they are the correct type of term.

self::assertContainsOnlyInstancesOf(TermInterface::class, $tags);
foreach ($tags as $tag) {
  self::assertSame('tags', $tag->bundle());
}

To assert the tags array only includes taxonomy terms, use self::assertContainsOnlyInstancesOf(), and to check each term has the correct term type, loop over each term and use self::assertSame().

Next, add some new assertions to the test to check the tag names match the specified tags.

self::assertSame('Drupal', $tags[0]->label());
self::assertSame('PHP', $tags[1]->label());
self::assertSame('Testing', $tags[2]->label());

These should pass as we already have code for them, but to see them fail, try changing a term type or tag name in the assertion or when creating the post to ensure the test works as expected.

Conclusion

In this lesson, you learned how to add the configuration required for tests by creating a custom test module with the required configuration and how to install it within the test so configuration, such as fields, are available.

You created PostBuilderTest - a Kernel test for testing PostBuilder.

In the next lesson, we'll look at unit testing and start to wrap up this course. {% endblock %}