oliverdavies.uk/source/_pages/atdc/5.md
2024-02-08 00:00:16 +00:00

7.8 KiB

title permalink
ATDC: Lesson 5 - Testing Post Ordering /atdc/5-testing-post-ordering

{% block head_meta %}

{% endblock %}

{% block content %} Now we have the repository in place from the last lesson, let's move on to testing that the posts are returned in the correct order.

We want them to be returned based on their published date and not by their node ID or anything else.

To do this, we will use a different type of test - a Kernel test.

Introducing Kernel tests

So far, we've been using Functional (or Browser) tests to ensure the blog page exists and that the correct posts are displayed.

It's easy to assert the correct posts are shown on the page, but it's much harder to assert they're shown in the right order.

This is much easier to do with a Kernel test (aka. an integration test).

Instead of making HTTP requests and checking the responses, we can test the results from the repository and ensure it returns the results in the correct order.

Writing your first Kernel test

Let's create a new test that uses the PostNodeRepository to find the nodes and assert we get an expected number returned.

<?php

namespace Drupal\Tests\atdc\Kernel;

use Drupal\atdc\Repository\PostNodeRepository;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;

class PostNodeRepositoryTest extends EntityKernelTestBase {

  public function testPostsAreReturnedByCreatedDate(): void {
    // Arrange.

    // Act.
    $postRepository = $this->container->get(PostNodeRepository::class);
    assert($postRepository instanceof PostNodeRepository);
    $nodes = $postRepository->findAll();

    // Assert.
    self::assertCount(3, $nodes);
  }

}

As with the Functional test, the file and class name must have a Test suffix, and test methods should have a test prefix. As we're testing the PostNodeRepository class, the convention is to name the test PostNodeRepositoryTest.

As this is a Kernel test, it should be placed within the tests/src/Kernel directory and extend the EntityKernelTestBase class.

We could extend others, such as the regular KernelTestBase, but as we'll be working with nodes, EntityKernelTestBase is the better option.

Instead of making assertions based on the HTTP response, we're testing what's returned from the findAll() method.

Resolving setup test failures

Run the tests to see the first error:

Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "Drupal\atdc\Repository\PostNodeRepository".

Although you created the PostNodeRepository in the previous lesson and added it as a service, it's not found.

It's defined within atdc.services.yml, but we need to be explicit about which modules are enabled when running the test, and the atdc module isn't enabled.

Create a $modules array within the test class and add atdc:

protected static $modules = ['atdc'];

Run the tests again, and you should get a different error:

Drupal\Component\Plugin\Exception\PluginNotFoundException: The "node" entity type does not exist.

As well as atdc, you must enable the node module. You can do so by adding it to the $modules array:

protected static $modules = ['node', 'atdc'];

Now, you should get a logic error instead of a setup error:

Failed asserting that actual size 0 matches expected size 3.

Typically, Kernel tests have more setup steps, such as installing module configuration and creating specific database tables.

But, although they can be more complicated to set up, they're faster to run compared to Functional tests.

The type of test you pick will depend on what you're trying to test.

Creating posts

Currently, the 'Arrange' step within the test is empty, and whilst we're asserting there should be three posts returned, none are.

We need to create some posts within the test.

To access the createNode() method we've used in previous lessons within a Kernel test, you must use the NodeCreationTrait.

Add use NodeCreationTrait within the test class and use Drupal\Tests\node\Traits\NodeCreationTrait; as an import if needed.

Within the test, you can use $this->createNode() to create posts.

Create the three posts the test is expecting:

// Arrange.
$this->createNode(['type' => 'post']);
$this->createNode(['type' => 'post']);
$this->createNode(['type' => 'post']);

This should be enough for the test to pass.

Adding assertions for the order

Next, let's assert they're returned in a specific order.

Update the posts to have a specific title and created date so we can specify which order we expect them to be returned in and which titles they should have:

// Arrange.
$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',
]);

Note we're intentionally setting them to be in an incorrect order, to begin with, so the test doesn't pass accidentally. This way, we can see it fail and know the task is complete once it passes.

Next, assert that the titles are returned in the correct order.

self::assertSame(
  ['Post two', 'Post one', 'Post three'],
  array_map(
    fn (NodeInterface $node) => $node->label(),
    $nodes
  )
);

For each node in $nodes, get its label (title) and compare them with the titles in the order we want.

As expected, the test fails:

1) Drupal\Tests\atdc\Kernel\PostNodeRepositoryTest::testPostsAreReturnedByCreatedDate
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
-    0 => 'Post two'
     1 => 'Post one'
-    2 => 'Post three'
+    2 => 'Post two'
+    3 => 'Post three'
 )

We want Post two to be returned first, followed by Post one and Post three.

Fixing the ordering

We need to update the code within PostNodeRepository to fix the ordering.

After loading the nodes, we need to sort them.

public function findAll(): array {
  $nodeStorage = $this->entityTypeManager->getStorage('node');
  $nodes = $nodeStorage->loadMultiple();

  uasort($nodes, function (NodeInterface $a, NodeInterface $b): int {
    return $a->getCreatedTime() <=> $b->getCreatedTime();
  });

  return $nodes;
}

This sorts the nodes based on their created time in the desired order and returns them.

This gets us further, but the test is still failing.

Whilst the order is correct, the array keys don't match what we expect:

1) Drupal\Tests\atdc\Kernel\PostNodeRepositoryTest::testPostsAreReturnedByCreatedDate
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
 Array &0 (
-    0 => 'Post two'
+    2 => 'Post two'
     1 => 'Post one'
-    2 => 'Post three'
+    3 => 'Post three'
 )

Finally, replace return $nodes; with return array_values($nodes) to reset the keys before returning them.

This should give us a passing test:

OK (1 test, 3 assertions)

And, because the correct titles are still being shown, our original Functional tests still pass, too:

OK (3 tests, 16 assertions)

Tip: to see the names of the tests in your output, add the --testdox flag to the phpunit command:

Blog Page (Drupal\Tests\atdc\Functional\BlogPage)
 ✔ Blog page
 ✔ Posts are visible

Post Node Repository (Drupal\Tests\atdc\Kernel\PostNodeRepository)
 ✔ Posts are returned by created date

Conclusion

In today's lesson, you learned about Kernel tests and wrote one to test the ordering of the posts returned from the PostNodeRepository.

Tomorrow, we'll refactor PostNodeRepositoryTest to use a Builder class and a custom assertion method. {% endblock %}