4.8 KiB
title | permalink |
---|---|
ATDC: Lesson 6 - Builders and custom assertions | /atdc/6-builders-custom-assertions |
{% block head_meta %}
{% endblock %}{% block content %}
In yesterday's lesson, you created your first Kernel test and used it to ensure the posts are returned from PostNodeRepository
in the desired order.
This is how we're creating the posts currently:
$this->createNode([
'created' => (new DrupalDateTime('-1 week'))->getTimestamp(),
'title' => 'Post one',
'type' => 'post',
]);
$this->createNode([
'created' => (new DrupalDateTime('-8 days'))->getTimestamp(),
'title' => 'Post two',
'type' => 'post',
]);
$this->createNode([
'created' => (new DrupalDateTime('yesterday'))->getTimestamp(),
'title' => 'Post three',
'type' => 'post',
]);
The Builder pattern is another design pattern I like, which makes it easier to build complex objects.
Let's create a Builder class to create the posts.
Creating a PostBuilder class
This is how I'd like to create a post using a PostBuilder
:
PostBuilder::create()
->setCreatedDate('-1 week')
->setTitle('Post one')
->getPost();
This makes it easier to do by creating named methods for each value we want to set and not relying on array keys whilst also moving implementation details like using DrupalDateTime
to set the created
date.
To do this, create a new class at src/Builder/PostBuilder.php
:
<?php
// web/modules/custom/atdc/src/Builder/PostBuilder.php
namespace Drupal\atdc\Builder;
final class PostBuilder {
public static function create(): self {
return new self();
}
}
It should be within the Drupal\atdc\Builder
namespace and has a static create
method that works as a named constructor and makes PostBuilder::create()
work.
As it returns a new version of self
, you can also chain methods onto it.
Add the additional methods and properties:
private ?DrupalDateTime $created = NULL;
private string $title;
public function setCreatedDate(string $time = 'now'): self {
$this->created = new DrupalDateTime($time);
return $this;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
Again, by returning $this
, we can keep chaining methods.
Finally, create the getPost()
method that creates the node based on the property values, saves it, and returns it.
public function getPost(): NodeInterface {
$post = Node::create([
'created' => $this->created?->getTimestamp(),
'title' => $this->title,
'type' => 'post',
]);
$post->save();
return $post;
}
Now, refactor the test to use the PostBuilder
:
PostBuilder::create()
->setCreatedDate('-1 week')
->setTitle('Post one')
->getPost();
PostBuilder::create()
->setCreatedDate('-8 days')
->setTitle('Post two')
->getPost();
PostBuilder::create()
->setCreatedDate('yesterday')
->setTitle('Post three')
->getPost();
Doing this simplifies the test and makes it easier to extend in the future by adding more methods to PostBuilder
.
Creating a custom assertion
Finally, for today, let's refactor the assertion that verifies the titles are returned in the correct order.
This is the current assertion:
self::assertSame(
['Post two', 'Post one', 'Post three'],
array_map(
fn (NodeInterface $node) => $node->label(),
$nodes
)
);
We create an array of expected titles and compare that to an array created from array_map
.
We can make this more reusable and readable by extracting this into a new custom assertion, which is just another static method.
Create a new static function at the bottom of the class with a name that describes what it's asserting:
/**
* @param array<int, string> $expectedTitles
* @param array<int, NodeInterface> $nodes
*/
private static function assertNodeTitlesAreSame(
array $expectedTitles,
array $nodes,
): void {
self::assertSame(
$expectedTitles,
array_map(
fn (NodeInterface $node) => $node->label(),
$nodes
)
);
}
We can add arguments for the arrays of titles and nodes, and be explicit about what they contain by adding a docblock.
In this method, we can do the same logic and use array_map
to create a list of node titles and compare them to the expected titles.
The benefits are that this now has a name that describes what we're asserting, and because it's a separate method, it can be reused in the same test or moved to a base class and used elsewhere.
Finally, refactor the test to use the new assertion:
self::assertNodeTitlesAreSame(
['Post two', 'Post one', 'Post three'],
$nodes,
);
In my opinion, this is a lot better.
In tomorrow's lesson, let's add some more tests to the PostNodeRepository
that we skipped in previous lessons.
{% endblock %}