oliverdavies.uk/source/_pages/atdc/6.md

193 lines
4.8 KiB
Markdown
Raw Normal View History

2024-02-07 22:25:53 +00:00
---
2024-02-08 00:00:16 +00:00
title: 'ATDC: Lesson 6 - Builders and custom assertions'
2024-02-07 22:25:53 +00:00
permalink: /atdc/6-builders-custom-assertions
---
{% block head_meta %}
<meta name="robots" content="noindex">
{% 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:
```language-php
2024-02-07 22:25:53 +00:00
$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`:
```language-php
2024-02-07 22:25:53 +00:00
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`:
```language-php
2024-02-07 22:25:53 +00:00
<?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:
```language-php
2024-02-07 22:25:53 +00:00
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.
```language-php
2024-02-07 22:25:53 +00:00
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`:
```language-php
2024-02-07 22:25:53 +00:00
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:
```language-php
2024-02-07 22:25:53 +00:00
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:
```language-php
2024-02-07 22:25:53 +00:00
/**
* @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:
```language-php
2024-02-07 22:25:53 +00:00
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 %}