Lab #2: Writing kernel tests - WidgetsBurritos/drupal-test-writing GitHub Wiki
Problem Statement
In Lab 1 you fixed an issue with MyMessageController::getMessageForUser() and added some basic unit test coverage to confirm the correct string was returned to the user. While you were fixing the code, you noticed the controller conditionally writes messages to the system log through a custom logging service, if that feature is turned on in the admin panel. You want to verify that your controller and logging service are working together as expected in these various conditions.
The Decision: Write a Kernel Test
For this, you will need an integration test to confirm MyMessageController::getMessageForUser()
and MyLogger::log()
are all working correctly together. You decide to use a kernel test here. Another smart choice!
We want to add kernel tests to cover the following eight scenarios (Don't worry. It sounds like a lot, but once we figure out one, the rest is mostly just copy/paste with minor tweaks):
- Tests admin user messages are properly logged if logging enabled.
- Tests admin user messages aren't logged if logging disabled.
- Tests
my super secret privilege
user messages are properly logged if logging enabled. - Tests
my super secret privilege
user messages aren't logged if logging disabled. - Tests
yet another privilege
user messages are properly logged if logging enabled. - Tests
yet another privilege
user messages aren't logged if logging disabled. - Tests regular user messages are properly logged if logging enabled.
- Tests regular user messages aren't logged if logging disabled.
Writing the Kernel Tests
-
Create
my_testing_module/tests/src/Kernel/Controller/MyMessageControllerTest.php
-
Let's start with a very basic kernel test:
<?php namespace Drupal\Tests\my_testing_module\Kernel\Controller; use Drupal\KernelTests\KernelTestBase; use Drupal\my_testing_module\Controller\MyMessageController; /** * Tests the functionality of my message controller. * * @group my_testing_module */ class MyMessageControllerTest extends KernelTestBase { /** * This is a temporary test just to ensure things are working as expected. */ public function testKernelTestIsWorking() { $this->assertEquals('a', 'a'); } }
-
Let's verify we can actually run this test:
ddev ssh cd /var/www/html php web/core/scripts/run-tests.sh --color --verbose --sqlite /tmp/a.sqlite --non-html --class 'Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest'
If everything is set up correctly we should see:
---- Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest ---- Status Group Filename Line Function -------------------------------------------------------------------------------- Pass Other MyMessageControll 17 Drupal\Tests\my_testing_module\Kern
-
We know we're going need to install a few modules for our test. The more modules you install in a test, the longer it will take to run, so it's best to keep this lean. As such, you should just start with the module you're absolutely sure you need and then build on from there. In this particular case, you add the following to your class:
public static $modules = ['my_testing_module', 'dblog', 'user'];
my_testing_module
is needed because it's the main module under testdblog
is needed because it's the module that handles logginguser
is needed because the results are dependent on user permissions
-
Delete the temporary
testKernelTestIsWorking()
-
Add a new method for our first test case:
/** * Tests admin user messages are properly logged, if logging enabled. * * @covers \Drupal\my_testing_module\Controller\MyMessageController::displayMessage */ public function testGetMessageForAdminUserLogsMessagesWhenSet() { }
-
Add the following to the test method to attempt to set the appropriate system configuration to enable logging:
// Enable logging. $this->config(MyMessageController::SETTINGS) ->set('log_users', TRUE) ->save();
-
At this point, let's try rerunning the test again. You should see an error like this:
1) Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest::testGetMessageForAdminUserLogsMessagesWhenSet Drupal\Core\Config\Schema\SchemaIncompleteException: No schema for my_testing_module.settings
Any module that uses configuration settings in kernel tests need to have a configuration schema defined. We won't spend time in this training going over how configuration schemas works, but read Configuration schema/metadata on Drupal.org if you have further questions about it.
-
For now, we can solve this problem by defining a schema for our module settings. To do so, create
my_testing_module/config/schema/my_testing_module.schema.yml
and set file contents to:my_testing_module.settings: type: config_object label: 'My Testing Module settings' mapping: log_users: type: boolean label: 'Log Users?'
-
If we rerun the tests now, it will fail again, but this time, it will because this is a "Risky Test". This just means that there are no assertions in your test, at the moment, which is fine.
---- Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest ---- Status Group Filename Line Function -------------------------------------------------------------------------------- Fail Other MyMessageControll 25 Drupal\Tests\my_testing_module\Kern Risky Test /var/www/html/vendor/phpunit/phpunit/src/Framework/TestResult.php:890
-
Next, we want to attempt to create an admin user. To add users in entity tests, we have a few different ways of doing it. We can either swap
KernelTestBase
toEntityKernelTestBase
and use methods there to do it, or we can make use of theUserCreationTrait
provided by core. Since users are the only entities we'll be working with we'll stick with the Trait for now, but you should definitely experiment withEntityKernelTestBase
in the future.-
Add this
use
statement for the trait to the top of the file:use Drupal\Tests\user\Traits\UserCreationTrait;
-
Use the trait for our test class:
use UserCreationTrait;
-
Setup the admin user inside of our test:
// Setup Admin User (UID=1). $this->setUpCurrentUser(['uid' => 1]); $controller = MyMessageController::create($this->container);
-
-
If we rerun the test again we should see an error like this:
Caused by PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'db.test49888175watchdog' doesn't exist
While we installed the
dblog
module previously, we never actually created the database tables. -
To create the
watchdog
table, add asetUp()
method to our test class:/** * {@inheritdoc} */ protected function setUp() { parent::setUp(); $this->installSchema('dblog', ['watchdog']); }
If we rerun the tests now, they should pass again.
-
Attempt to verify the displayed message.
// Confirm response. $expected = [ '#markup' => implode('<br>', [ 'You are logged in.', 'You are special.', 'You have yet another privilege.', ]), ]; $this->assertEquals($expected, $controller->displayMessage());
-
Now if we rerun the tests we should find ourselves in a somewhat baffling situation:
1) Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest::testGetMessageForAdminUserLogsMessagesWhenSet Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - '#markup' => 'You are logged in.<br>You are special.<br>You have yet another privilege.' + '#markup' => 'You are logged in.' )
- We aren't seeing any exceptions thrown or error messages, but we are expecting to have 3 messages for admin users, but we only have 1 showing up.
- The unit tests prove users with full admin permissions get 3 messages.
- Testing this manually in our browser, we get the correct result.
- So what is happening?
- This behavior indicates that we're missing something, but what?
-
A good way to figure out what is missing is by looking at dependency lists for any modules you're using. In this particular case, we see that the
user
module is dependent on thesystem
module. Kernel tests don't install dependencies for us, so we should try adding it to the module list:public static $modules = ['my_testing_module', 'dblog', 'user', 'system'];
If we rerun the tests now, they pass again. Phew!
-
The above assertion is just for convenience. We are being a bit redundant with the unit tests from Lab 1. What we really want to test here is the integration with the logging service. The easiest way to tell if the logging service is working is that it adds rows to the database.
For additional convenience, here is a helper method which can be used to retrieve the
$row_ct
most recent rows, matching the specified severity level, from thewatchdog
table./** * Helper method to retrieve the last watchdog message by severity. * * @param string $severity * Message severity. * @param int $row_ct * Number of rows to return. * * @return array * Associative array containing messages. */ protected function getLastWatchdogRowsBySeverity($severity, $row_ct = 1) { $query = $this->container->get('database')->select('watchdog', 'w'); $query->fields('w', ['message']); $query->condition('w.type', 'my_testing_module'); $query->condition('w.severity', $severity); $query->orderBy('w.timestamp', 'DESC'); $query->range(0, $row_ct); return $query->execute()->fetchAll(\PDO::FETCH_ASSOC); }
-
We can use the above helper method to now set our expectation:
- Add this use statement to the top of your file:
use Drupal\Core\Logger\RfcLogLevel;
- Set our expectations:
// Confirm log messages. $log_messages = $this->getLastWatchdogRowsBySeverity(RfcLogLevel::INFO, 3); $expected = [ ['message' => 'super secret privilege granted'], ['message' => 'yet another privilege granted'], ]; $this->assertEquals($expected, $log_messages);
- Add this use statement to the top of your file:
-
Now run the test one last time. If all went well we should have passing tests:
---- Drupal\Tests\my_testing_module\Kernel\Controller\MyMessageControllerTest ---- Status Group Filename Line Function -------------------------------------------------------------------------------- Pass Other MyMessageControll 55 Drupal\Tests\my_testing_module\Kern
-
Copy and paste this method and change the various expectations for each of the other desired test cases.
Need Assistance?
If you're stuck, have a look at Pull Request #3