352 lines
10 KiB
Markdown
352 lines
10 KiB
Markdown
---
|
|
title: 'ATDC: Lesson 8 - Tagging posts and test configuration'
|
|
permalink: /atdc/8-tagging-posts-test-configuration
|
|
---
|
|
|
|
{% block head_meta %}
|
|
<meta name="robots" content="noindex">
|
|
{% 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`:
|
|
|
|
```language-php
|
|
<?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:
|
|
|
|
```language-php
|
|
/** @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`:
|
|
|
|
```language-php
|
|
/** @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:
|
|
|
|
```language-yaml
|
|
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:
|
|
|
|
```language-plain
|
|
./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`:
|
|
|
|
```language-yaml
|
|
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`:
|
|
|
|
```language-yaml
|
|
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`:
|
|
|
|
```language-php
|
|
protected static $modules = [
|
|
// Core.
|
|
'node',
|
|
|
|
// Custom.
|
|
'atdc_test',
|
|
];
|
|
```
|
|
|
|
Finally, install the configuration to create the field. Add this within the test:
|
|
|
|
```language-php
|
|
$this->installConfig(modules: [
|
|
'atdc_test',
|
|
]);
|
|
```
|
|
|
|
After adding this and attempting to install the configuration to add the field, you'll get an error:
|
|
|
|
```language-plain
|
|
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:
|
|
|
|
```language-yaml
|
|
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:
|
|
|
|
```language-php
|
|
$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:
|
|
|
|
```language-php
|
|
$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`:
|
|
|
|
```language-php
|
|
/**
|
|
* @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.
|
|
|
|
```language-php
|
|
$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.
|
|
|
|
```language-php
|
|
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.
|
|
|
|
```language-php
|
|
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 %}
|