Hello World page test

The first Functional test we will write is for the Hello World page we created and the functionality behind it. We will test whether the page shows the correct Hello World message, also depending on the value found in the configuration. So let's create the class for it, naturally in the hello_world module, inside the tests/src/Functional folder:

namespace Drupal\Tests\hello_world\Functional; 
 
use Drupal\Tests\BrowserTestBase; 
 
/** 
 * Basic testing of the main Hello World page. 
 * 
 * @group hello_world 
 */ 
class HelloWorldPageTest extends BrowserTestBase {}  

You can really see the consistency with the other types of tests. But in this case, as mentioned, we extend from BrowserTestBase.

Also, like before, we can configure a number of modules we want installed:

/** 
 * Modules to enable. 
 * 
 * @var array 
 */ 
protected static $modules = ['hello_world', 'user']; 

We will need the User module for the second test we run, which will go in the same class as this one. But let's proceed with the first, easier test:

/** 
 * Tests the main Hello World page. 
 */ 
public function testPage() { 
  $expected = $this->assertDefaultSalutation(); 
  $config = $this->config('hello_world.custom_salutation'); 
  $config->set('salutation', 'Testing salutation'); 
  $config->save(); 
 
  $this->drupalGet('/hello'); 
  $this->assertSession()->pageTextNotContains($expected); 
  $expected = 'Testing salutation'; 
  $this->assertSession()->pageTextContains($expected); 
}  

If you remember, our /hello page shows a greeting depending on the time of day, unless an administrator has overridden that message through a configuration form. So we start this test by asserting that with a fresh install that has no override, we see the time-based greeting. And for that we create a separate assertion message since it's a bit wordy and we will reuse it:

/** 
 * Helper function to assert that the default salutation is present on the page. 
 * 
 * Returns the message so we can reuse it in multiple places. 
 */ 
private function assertDefaultSalutation() { 
  $this->drupalGet('/hello'); 
  $this->assertSession()->pageTextContains('Our first route'); 
  $time = new \DateTime(); 
  $expected = ''; 
  if ((int) $time->format('G') >= 00 && (int) $time->format('G') < 12) { 
    $expected = 'Good morning'; 
  } 
 
  if ((int) $time->format('G') >= 12 && (int) $time->format('G') < 18) { 
    $expected = 'Good afternoon'; 
  } 
 
  if ((int) $time->format('G') >= 18) { 
    $expected = 'Good evening'; 
  } 
  $expected .= ' world'; 
  $this->assertSession()->pageTextContains($expected); 
  return $expected; 
} 

The very first thing we do here is use the drupalGet() method to navigate to a path on the site. Do check out the method signature for all the options you can pass to it. And the first assertion we make is that the page contains the text Our first route (which is the page title). The parent assertSession() method returns an instance of WebAssert which contains all sorts of methods for asserting the presence of elements on the current page in the Mink session. One such method is the generic pageTextContains() with which we simply check that the given text can be found anywhere on the page.

Although in quite a lot of cases asserting the presence of a text string is enough, you may want to ensure that it is actually the right one (to avoid false positives). For example, in our case, we could check that it is really the page title that is rendered inside an <h1> tag. We can do it like so:

$this->assertSession()->elementTextContains('css', 'h1', 'Our first route');  

The elementTextContains() method can be used to find an element on the page based on a locator (CSS selector or xpath) and assert that it contains the specified text. In our example we use the CSS selector locator and we try to find the <h1> element.

If all of that is okay, we proceed with asserting that the actual salutation message is present on the page. Unfortunately, we have to duplicate quite some code because it is dependent on the time of day. A good homework for you would be to extract this logic to a service that determines the message and use this service both here and in the actual code. And since we need this message later, we also return it.

Going back to our actual test method, we can proceed knowing that the message is showing correctly on the page. And the next thing we want to test is the following: if there is a hello_world.custom_salutation configuration object with a salutation value, that is what should be shown. So we programmatically create it. Next, we again navigate to the same path (we essentially reload the page) and check that the old message is not shown anymore and that the new one is instead.

So if we actually run this test:

../vendor/bin/phpunit ../modules/custom/hello_world/tests/src/Functional/HelloWorldPageTest.php  

...darn. We get an error:

Behat\Mink\Exception\ResponseTextException: The text "Good evening world" appears in the text of this page, but it should not. 

It's as if we didn't even override the salutation message. But we did.

The problem is caching. Keep in mind, we are navigating these pages as anonymous users and caching is enabled on the site like in normal scenarios. In Chapter 11, Caching, I made a note about this particular problem—the max-age property only bubbles up to the page level for the dynamic page cache (logged-in users) and not for anonymous users.

This is a great example of automated testing shedding light on mistakes we introduce while developing and that we don't notice. We most likely wrote our functionality while having caching disabled and/or always visiting the page as a logged-in user. So it's an easy mistake to make. Luckily, automated testing comes to the rescue.

The solution to this problem can be found using an all-out cache kill switch. This means that we need to alter a bit our logic to tell Drupal to never cache the pages where our salutation component is shown. This is the price we have to pay for the highly dynamic nature of our functionality and it's always a good exercise to evaluate if it is worth it.

The kill switch is actually easy to use. It's a service that we need to inject into our HelloWorldSalutation service:

/** 
 * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch 
 */ 
protected $killSwitch; 
 
/** 
 * HelloWorldSalutation constructor. 
 * 
 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory 
 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher 
 * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $killSwitch 
 */ 
public function __construct(ConfigFactoryInterface $config_factory, EventDispatcherInterface $eventDispatcher, KillSwitch $killSwitch) { 
  $this->configFactory = $config_factory; 
  $this->eventDispatcher = $eventDispatcher; 
  $this->killSwitch = $killSwitch; 
}  

And the appropriate use statement at the top:

use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;  

And at the beginning of both the getSalutation() and getSalutationComponent() methods, we simply have to add this line:

$this->killSwitch->trigger();  

This will tell Drupal's internal page cache to never cache this page. But before we go running the test again, we mustn't forget to add the page_cache_kill_switch service as a dependency to the HelloWorldSalutation service inside hello_world.services.yml. And now if we run this test, we should get a green result.