193 lines
4.7 KiB
Markdown
193 lines
4.7 KiB
Markdown
|
---
|
||
|
title: Lesson 6 - Builders and custom assertions
|
||
|
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:
|
||
|
|
||
|
```php
|
||
|
$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`:
|
||
|
|
||
|
```php
|
||
|
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
|
||
|
<?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:
|
||
|
|
||
|
```php
|
||
|
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.
|
||
|
|
||
|
```php
|
||
|
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`:
|
||
|
|
||
|
```php
|
||
|
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:
|
||
|
|
||
|
```php
|
||
|
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:
|
||
|
|
||
|
```php
|
||
|
/**
|
||
|
* @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:
|
||
|
|
||
|
```php
|
||
|
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 %}
|