A first functional test in Drupal 9 (test a redirect in a controller)

...of course this took longer than anticipated, but I have achieved this milestone, writing and executing a complete functional test in Drupal. It was not an easy, trivial test - and the path I took in implementing it highlights the gotchas and difficulty of writing such tests. As well, my process is likely not be unique, and the challenges I faced appear common. It there appears valuable for me to put down a quick writeup on quirks and tricks I saw along the way. In this article, I assume the reader is an intermediate Drupal developer. I will not delve on the basics of Drupal development.

~ * ~ * ~ * ~

First things first - and this took me longer than I'd like to admit. There is a big difference between <?php and <? , the latter being a convenience shorthand for php code. I recommend always using the former. The reason is, apache2 (or nginx) php executable is configured differently from the command-line php executable. They are configured in these two files, respectively:

  • /etc/php/8.1/cli/php.ini
  • /etc/php/8.1/apache2/php.ini

And even though you can enable the short tags:

short_open_tag = On

You may forget to do so, or across the multitude of environments and servers, it may be just inconvenient and error-prone to set this, twice, every time. Just be warned that if your site renders just fine with <? , it does not mean that drush or tests will work at all.

A common tell-tale sign of this problem is if you run drush, or phpunit, and instead of expected output you see a dump of some php code, particularly the contents of settings.php file. It means the php interpreter failed to read the file - so, your short tags are not enabled.

Now that this is out of the way - let's write some Drupal code!

~ * ~ * ~ * ~

We'll write production code first, and test is second. To the readers who do it in reverse: good for you, but I found not much benefit to doing that. In fact, if I'm stuck, I'd rather production code works, and I can figure the tests later. If I'm stuck in tests and the production code doesn't work at all - I'd say the next step is to re-focus and attempt to fix mistakes and write code that works - that is to say, production code. And yes, sometimes I write test code before production code. But often, not.

We are going to write, and test, a simple controller that redirects a url of pattern "/worklogs/{date}" to a corresponding node with field_date={date} . The code is pretty simple:

<?php
// web/modules/ish_drupal_module/src/Controller/WorklogsController.php

namespace Drupal\ish_drupal_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\node\Entity\Node;

class WorklogsController extends ControllerBase {

  public function showRedirect($year) {
    $nids = \Drupal::entityQuery('node')
      ->condition('field_date', $year)
      ->range(0, 1)
      ->execute();

    if (!empty($nids)) {
      $nid = reset($nids);
      $node = Node::load($nid);
      return new RedirectResponse($node->toUrl()->toString());
    }
    return new RedirectResponse('/worklog');
  }

}

^ this goes into a module, something you presumably already have setup. My module is called ish_drupal_module, and the name follows a formalism that I re-use elsewhere.

~ * ~ * ~ * ~

Now let's follow several iterations of testing, the first of which did not work. I started with the following test:

<?php
// web/modules/ish_drupal_module/tests/src/Functional/WorklogsControllerTest.php

namespace Drupal\Tests\ish_drupal_module\Functional;

use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;

class WorklogsControllerTest extends BrowserTestBase {
  protected $defaultTheme = 'stark';
  protected static $modules = ['node', 'ish_drupal_module', 'user'];
  protected $user;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    //                                    permissions, name, is_admin
    $this->user = $this->drupalCreateUser([], NULL, TRUE);
    $this->user->addRole('administrator');
  }

  /**
   * Tests the redirect
  **/
  public function testShowRedirect() {
    $node = Node::create([
      'type' => 'worklog',
      'title' => 'Test Node 2025a',
      'field_date' => '2025a',
      'status' => 1,
    ]);
    $node->save();
    $saved_node = Node::load($node->id());
    $this->assertNotNull($saved_node, 'Node was saved successfully.');

    $this->drupalLogin($this->user);
    $current_user = \Drupal::currentUser();
    $this->assertEquals($this->user->id(), $current_user->id(), 'User is logged in.');

    $this->assertSession()->addressEquals($node->toUrl()->toString());
    $this->assertSession()->statusCodeEquals(200);

    $this->assertSession()->pageTextContains('Test Node 2025a');
  }
}

In order to run it, I had to install a bunch of stuff via composer. 

Note: I changed minimum stability from "stable" to "dev" in composer.json . This should not be done in production.

Issuing a combination of any or all of the following commands to install the libraries, eventually worked for me:

 composer require --dev --no-update symfony/filesystem:4.4.42
 composer require --dev drupal/core-dev:9.5.11 --with-all-dependencies
   composer require --dev behat/mink jcalderonzumba/mink-phantomjs-driver
   composer update -W
   composer update symfony/filesystem:4.4.42 drush/drush -W
   composer require --dev drupal/core-dev -W
   composer require --dev phpspec/prophecy-phpunit:^2

Obviously, I copied the file phpunit.xml from the core, and adjusted some values.

With that, I was ready to run the test as follows:

./vendor/bin/phpunit -c phpunit.xml  -d memory_limit=1G web/modules/ish_drupal_module/tests/src/Functional/WorklogsControllerTest.php --debug

This did not work - giving me a 403 permission denied, instead of the expected 200 ok. I added the assertions that (1) the node saved, and (2) the user logged in, to make sure I'm in an authenticated environment.

Incidentally, the fact assertion passed that node saved, did not help me!

Next, I introduced a bunch of logging to see why exactly I was getting the 403 code:


$response = $this->getSession()->getDriver()->getClient()->request('GET', '/worklogs/2025a', [], [], ['max_redirects' => 0]);
echo('+++ $response');
var_dump($response);

And it revealed the problem: field 'field_date' was not present in test! While the field was present in production, the production database does not get copied to the test environment, so the fields created in the UI are not available. 

The next and last step was to add the necessary field in test, and after that my test passed:

  protected function setUp(): void {
    parent::setUp();

    if (!NodeType::load('worklog')) {
      NodeType::create([
          'type' => 'worklog',
          'name' => 'Worklog',
      ])->save();
    }
    if (!FieldStorageConfig::loadByName('node', 'field_date')) {
      FieldStorageConfig::create([
        'field_name' => 'field_date',
        'entity_type' => 'node',
        'type' => 'string',
      ])->save();
    }
    if (!FieldConfig::loadByName('node', 'worklog', 'field_date')) {
      FieldConfig::create([
        'field_name' => 'field_date',
        'entity_type' => 'node',
        'bundle' => 'worklog',
        'label' => 'Date',
      ])->save();
    }
    
    ...
  }

.

And so my entire test file looked like this: https://github.com/wasya-co/ish_drupal_module/blob/0.5.0/tests/src/Functional/WorklogsControllerTest.php

I hope this helps somebody develop better in Drupal! Lastly, I am available for project-based Drupal work, so if you need some Drupal development done, hit the "contact us" button anywhere on this site to reach out.

.^.

Please login or register to post a comment.