Lab #3: Writing system tests with BrowserTestBase and Nightwatch.js - WidgetsBurritos/drupal-test-writing GitHub Wiki
Part 1: The Security Bug
Problem Statement
Oh no! You've just discovered a pretty major security flaw in your module. Unauthenticated users can access /my-message
even though this functionality was supposed to be accessible for authenticated users.
The Proposed Fix
To fix it, you are proposing changing the route in my_testing_module.routing.yml
from:
my_testing_module.my_message:
path: 'my-message'
defaults:
_controller: '\Drupal\my_testing_module\Controller\MyMessageController::displayMessage'
_title_callback: '\Drupal\my_testing_module\Controller\MyMessageController::title'
requirements:
_permission: 'access content'
To this:
my_testing_module.my_message:
path: 'my-message'
defaults:
_controller: '\Drupal\my_testing_module\Controller\MyMessageController::displayMessage'
_title_callback: '\Drupal\my_testing_module\Controller\MyMessageController::title'
requirements:
_role: 'authenticated'
The Decision: Write a Functional Test
You decide just fixing the problem isn't enough. You want to add functional test coverage to ensure this code doesn't break again. Yet another great move!
Upon digging you discover there's already a functional test present in tests/src/Functional/MyFunctionalTest.php
.
- You decide to modify the existing test case to ensure a 200 status code.
- You determine you only need one more test case ensuring authenticated users receive a 403 status code.
Running the Tests
Before we begin, let's ensure the existing test is working as expected. We can do so by running this command:
php web/core/scripts/run-tests.sh --color --verbose --sqlite /tmp/a.sqlite --non-html --class 'Drupal\Tests\my_testing_module\Functional\MyFunctionalTest'
The response should contain the following text:
---- Drupal\Tests\my_testing_module\Functional\MyFunctionalTest ----
Status Group Filename Line Function
--------------------------------------------------------------------------------
Pass Other MyFunctionalTest. 44 Drupal\Tests\my_testing_module\Func
If it doesn't please ensure you followed all of the steps in the Getting Started documentation
Writing the Test
-
Add
$assert->statusCodeEquals(200);
to thetestMessageControllerIsLoadingForAuthenticatedUsers()
method. -
Rerun the test to ensure everything still works as expected.
-
Create a new test case method for our 403 test:
/** * Confirm unauthenticated users can't access controller route. */ public function testMessageControllerDoesntLoadForUnauthenticatedUsers() { }
-
Retrieve our WebAssert session object inside the new test:
$assert = $this->assertSession();
-
Navigate to the controller router:
$this->drupalGet('my-message');
-
Check for expected failure message and status code:
$assert->pageTextContains('You are not authorized to access this page.'); $assert->statusCodeEquals(403);
-
Rerun the tests. If you did everything right the response should contain:
---- Drupal\Tests\my_testing_module\Functional\MyFunctionalTest ---- Status Group Filename Line Function -------------------------------------------------------------------------------- Pass Other MyFunctionalTest. 44 Drupal\Tests\my_testing_module\Func Pass Other MyFunctionalTest. 55 Drupal\Tests\my_testing_module\Func
-
So both tests pass, but what you've just realized is by creating two separate tests, it's bootstraps Drupal twice. As functional tests are much slower than other types of test, you're willing to consolidate other multiple scenarios into a single test. To do so, copy this into the first test, follow these steps:
- Rename
testMessageControllerIsLoadingForAuthenticatedUsers()
totestMessageControllerProperlyHandlesAccessControls()
as that's a little bit more accurate what we're testing here. - Move this code into above the
$this->drupalLogin()
line in the original test:$this->drupalGet('my-message'); $assert->pageTextContains('You are not authorized to access this page.'); $assert->statusCodeEquals(403);
- Delete the testMessageControllerDoesntLoadForUnauthenticatedUsers() test method.
- For improved readability, add a comment to each section within the test to provide a plaintext explanation as to what is happening:
// Test unauthenticated users can't access the controller.
// Test authenticated users can access the controller.
- Rename
-
Rerun the tests one more time. If you did everything correctly, your tests should still be passing.
Part 2: The JavaScript Test
Problem Statement
As you continue to look at your module, you notice that the admin form has some custom javascript functionality execute when the checkbox is toggled that changes the color of the text label. Obviously functionality this important really needs to be tested. So you opt to build a test in Nightwatch.js. Whoa! There aren't a whole lot of Drupal devs doing that yet. You're ahead of the curve on this one.
Running the Tests
Before we begin let's ensure we can run the existing Nightwatch test.
ddev ssh
cd /var/www/html/web/core
yarn test:nightwatch ../modules/custom/my_testing_module/tests/src/Nightwatch
Writing the Test
-
Create a new setup fixture called
web/modules/custom/my_testing_module/tests/src/Nightwatch/fixtures/TestSiteInstallTestScript.php
. This file will be referenced by Nightwatch to bootstrap Drupal with the correct modules installed. (Note: Once this core issue is resolved, you won't need to do steps 1 and 2). -
Add the following code to install the my_testing_module
<?php namespace Drupal\TestSite; /** * Setup file used by Nightwatch tests. */ class TestSiteInstallTestScript implements TestSetupInterface { /** * {@inheritdoc} */ public function setup() { $modules = ['my_testing_module']; \Drupal::service('module_installer')->install($modules); } }
-
Modify the
MyNightwatchTest.js
file to use our Drupal bootstrap fixture by changing:browser .drupalInstall()
to:
browser .drupalInstall({ setupFile: __dirname + '/fixtures/TestSiteInstallTestScript.php', })
-
Delete the
'Visit the home page and ensure "Skip to main content" is present'
test case as that was merely to confirm nightwatch is working correctly. -
Create a new test case by adding this:
'Visit the message module settings and toggle log checkbox': (browser) => { // New code will be added here. // Clean up our session. browser .end(); }
-
Within the test case, login as an admin user and navigate to the settings page:
// Navigate to module admin setting. browser .drupalLoginAsAdmin() .drupalRelativeURL('/admin/config/system/my_testing_module/settings');
Rerun Nightwatch. If you did everything correctly tests should still be passing.
-
Confirm that
label[for=edit-log-users]
hasmy-unchecked-class
set and notmy-checked-class
:// Confirm label contains correct CSS class. browser .assert.cssClassPresent('label[for=edit-log-users]', 'my-unchecked-class') .assert.not.cssClassPresent('label[for=edit-log-users]', 'my-checked-class')
-
Retrieve the state of the checkbox by executing a command directly in the browser and then asserting the value is
false
:.execute(function() { return Drupal.myMessageLogging.myCheckbox.checked; }, [], function (result) { browser.assert.strictEqual(result.value, false); })
Rerun Nightwatch. If you did everything correctly tests should still be passing.
-
Toggle the checkbox state:
browser .click('input[name=log_users]')
-
Now reusing what we've learned above, confirm the checkbox state and label classes are correct. Then flip the checkbox state one more time, and confirm everything reverted back to normal.
Need Assistance?
If you're stuck, have a look at Pull Request #4