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

5.8 KiB

title permalink
Lesson 3 - Building a Blog /atdc/3-building-blog

{% block head_meta %}

{% endblock %}

{% block content %} In the previous two lessons, you've created a Drupal project with PHPUnit and have a running test suite that uses users and content created as part of each test.

With what you've learned, let's create a simple Blog module with tests and test-driven development.

Creating the Blog page

First, let's create a page that will list the posts. This will be similar to our first tests for Drupal's front and admin pages.

Create a new BlogPageTest and have it extend BrowserTestBase.

Let's assert that a page should exist at /blog by returning a 200 status code, as this should be accessible by anonymous users.

<?php

namespace Drupal\Tests\atdc\Functional;

use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;

class BlogPageTest extends BrowserTestBase {

  protected $defaultTheme = 'stark';

  public function testBlogPage(): void {
    $this->drupalGet('/blog');

    $this->assertSession()->statusCodeEquals(Response::HTTP_OK);
  }

}

As you haven't created it, the status code should be a 404 - causing the test to fail.

Tip: you can use --filter testBlogPage to run a single test or --stop-on-failure to stop running the tests as soon as an error occurs. These should shorten the time to run your tests, as you only run the tests you need.

Whilst you could create the page using the Views module, let's create a custom route.

Create an atdc.routing.yml file:

# web/modules/custom/atdc/atdc.routing.yml

atdc.blog:
  path: /blog
  defaults:
    _controller: Drupal\atdc\Controller\BlogPageController
    _title: Blog
  requirements:
    _permission: access content

With this added, the status code doesn't change and is a 404.

Like in the previous lesson, you need to enable the atdc module by setting $modules in your test:

protected static $modules = ['atdc'];

You'll also need to create an atdc.info.yml file so the module can be installed:

# web/modules/custom/atdc/atdc.info.yml

name: ATDC
type: module
core_version_requirement: ^10

This should change the status code to a 403, as you also need the node module for the access content permission:

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

This should cause the status code to change again - this time to a 500.

This is progress.

The atdc module is being installed and enabled, and its routing file is being loaded. But, it references a Controller class that doesn't exist yet.

Let's do that next.

Creating a BlogPageController

Create the expected Controller class within a src/Controller directory:

<?php

// web/modules/custom/atdc/src/Controller/BlogPageController.php

namespace Drupal\atdc\Controller;

class BlogPageController {

}

Note the namespace is different from the one within the test classes and we don't need to extend any other classes.

For this step, the simplest thing you can do to get a passing test is to return an empty render array.

As long as it's an array, even an empty one, the test should pass:

public function __invoke(): array {
  return [];
}

As a rule, you want the tests to pass as often and quickly as possible by doing the simplest thing to achieve it - even returning a hard-coded value or an empty array.

Now the test passes, you can add to it and drive out the next piece of functionality.

This is also a good time to do a git commit.

Asserting posts are visible

Again, let's start with a new test:

public function testPostsAreVisible(): void {
  // Arrange.
  $this->createNode(['type' => 'post', 'title' => 'First post']);
  $this->createNode(['type' => 'post', 'title' => 'Second post']);
  $this->createNode(['type' => 'post', 'title' => 'Third post']);

  // Act.
  $this->drupalGet('/blog');

  // Assert.
  $assert = $this->assertSession();
  $assert->pageTextContains('First post');
  $assert->pageTextContains('Second post');
  $assert->pageTextContains('Third post');
}

As we're returning an empty array within BlogPageController, the page will have no content and this test will fail with a message like:

  1. Drupal\Tests\atdc\Functional\BlogPageTest::testPostsAreVisible Behat\Mink\Exception\ResponseTextException: The text "First post" was not found anywhere in the text of the current page.

Start by extending the ControllerBase base class within your Controller:

+ use Drupal\Core\Controller\ControllerBase;
+
- class BlogPageController {
+ class BlogPageController extends ControllerBase {

Now, within the __invoke method, add this to return a list of each node title:

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

  $build = [];
  $build['content']['#theme'] = 'item_list';
  foreach ($nodes as $node) {
    $build['content']['#items'][] = $node->label();
  }

  return $build;
}

As the node titles are within the page content, the test should pass.

To be confident, try returning an empty array again or removing the foreach loop, seeing the test fail, and reverting the change.

Confidence comes from tests that pass and fail when expected, so you're sure the correct behaviour is being tested, and the tests aren't passing accidentally.

You can add further tests, such as checking that only nodes of a specified node type are returned. Currently, all nodes would be listed, even if they aren't posts.

Asserting posts are in the correct order

We have a list of post titles on a page and a test to prove it, but what if we want to ensure the posts are shown in a specified order?

That's harder to do with a functional test, so in the next lesson, we'll refactor the code and look at Kernel tests. {% endblock %}