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):

  1. Tests admin user messages are properly logged if logging enabled.
  2. Tests admin user messages aren't logged if logging disabled.
  3. Tests my super secret privilege user messages are properly logged if logging enabled.
  4. Tests my super secret privilege user messages aren't logged if logging disabled.
  5. Tests yet another privilege user messages are properly logged if logging enabled.
  6. Tests yet another privilege user messages aren't logged if logging disabled.
  7. Tests regular user messages are properly logged if logging enabled.
  8. Tests regular user messages aren't logged if logging disabled.

Writing the Kernel Tests

  1. Create my_testing_module/tests/src/Kernel/Controller/MyMessageControllerTest.php

  2. 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');
      }
    
    }
    
  3. 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
    
  4. 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 test
    • dblog is needed because it's the module that handles logging
    • user is needed because the results are dependent on user permissions
  5. Delete the temporary testKernelTestIsWorking()

  6. 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() {
    
      }
    
  7. 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();
    
  8. 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.

  9. 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?'
    
  10. 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
    
  11. 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 to EntityKernelTestBase and use methods there to do it, or we can make use of the UserCreationTrait 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 with EntityKernelTestBase 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);
      
  12. 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.

  13. To create the watchdog table, add a setUp() 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.

  14. 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());
    
  15. 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?
  16. 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 the system 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!

  17. 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 the watchdog 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);
      }
    
  18. 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);
      
  19. 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
    
  20. 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