Lab #1: Writing unit tests - WidgetsBurritos/drupal-test-writing GitHub Wiki

Problem Statement

As a new maintainer of the my_testing_module module, you've just discovered something wrong with MyMessageController::getMessageForUser().

  • It's supposed to show a message for any authenticated users that says: "You are logged in"
    • It's actually showing "You might be logged in" instead.
  • It's supposed to show a message for users with the my super secret privilege permission that says: "You are super special."
    • It's actually showing "You aren't all that special." instead.
  • It's supposed to show a message for users with the yet another privilege permission that says "You have yet another privilege."
    • This part is working.
  • If multiple scenarios apply, it should show all of the above messages.
    • It's actually just showing the first message it encounters.

The Proposed Fix

To fix it, you are proposing changing the code from this:

  /**
   * Retrieves the message for the specified user.
   *
   * @param \Drupal\Core\Session\AccountInterface $user
   *   User account.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
   *   User message.
   */
  public function getMessageForUser(AccountInterface $user) {
    if ($user->hasPermission('my super secret privilege')) {
      $this->log('info', 'super secret privilege granted');
      return $this->t("You aren't all that special.");
    }
    elseif ($user->hasPermission('yet another privilege')) {
      $this->log('info', 'yet another privilege granted');
      return $this->t('You have yet another privilege.');
    }
    else {
      $this->log('warning', 'unprivileged access');
    }
    return $this->t('You might be logged in.');
  }

To this:

  /**
   * Retrieves the message for the specified user.
   *
   * @param \Drupal\Core\Session\AccountInterface $user
   *   User account.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
   *   User message.
   */
  public function getMessageForUser(AccountInterface $user) {
    $messages = [$this->t('You are logged in.')];
    $has_elevated_permission = FALSE;
    if ($user->hasPermission('my super secret privilege')) {
      $this->log('info', 'super secret privilege granted');
      $has_elevated_permission = TRUE;
      $messages[] = $this->t('You are special.');
    }
    if ($user->hasPermission('yet another privilege')) {
      $this->log('info', 'yet another privilege granted');
      $has_elevated_permission = TRUE;
      $messages[] = $this->t('You have yet another privilege.');
    }
    if (!$has_elevated_permission) {
      $this->log('warning', 'unprivileged access');
    }
    return implode('<br>', $messages);
  }

Note: For now you can safely assume the user is actually logged in to begin with. We will deal with unauthenticated users in a later lab.

The Decision: Write a Unit Test

You decide just fixing the problem isn't enough. You want to add unit test coverage to ensure this code doesn't break again. Great decision!

Fortunately, there's already a Unit/Controller/MyMessageControllerTest that covers this class, so we don't have to start from scratch. We want to add tests for the following four conditions:

  1. The user only has the my super secret privilege permission.
  2. The user only has the yet another privilege permission.
  3. The user has both of the permissions set.
  4. The user doesn't have any of these permissions set.

Running the Tests

Before we begin, let's ensure the existing test is working as expected. We can do so by running this command:

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\Unit\Controller\MyMessageControllerTest'

The response should contain the following text:

---- Drupal\Tests\my_testing_module\Unit\Controller\MyMessageControllerTest ----


Status    Group      Filename          Line Function                            
--------------------------------------------------------------------------------
Pass      Other      MyMessageControll   71 Drupal\Tests\my_testing_module\Unit

If it doesn't please ensure you followed all of the steps in the Getting Started documentation

Reviewing the Existing Tests

Before writing any new tests for this class, let's take a look at what is already here:

  1. public function setUp()
    • $this->user is a stub user account that will always show the name "John Doe"
    • $this->config_factory is a stub config factory that always returns FALSE
    • $this->logger is a stub logging service
    • We're also calling $this->setStringTranslation($this->getStringTranslationStub()), which is a mechanism for mocking $this->t() methods.
  2. public function testTitleShowsCurrentUser()
    • This tests MyMessageController::title()
    • $controller is instantiated using the test doubles defined in setUp()
    • The same translation stub that we set on $this is getting set on $controller.
    • We then set the expectation that $controller->title() will say 'Hi John Doe.'

Writing the Unit Tests

  1. Create a new unit test method.

    /**
     * Confirms controller message is correct for users with my super secret privs.
     *
     * @covers \Drupal\my_testing_module\Controller\MyMessageController::getMessageForUser
     */
    public function testGetMessageForMySuperSecretPrivilegeIsCorrect() {
    }
    
  2. Instantiate the controller in our test and set the translation stub.

    $controller = new MyMessageController($this->user, $this->config_factory, $this->logger);
    $controller->setStringTranslation($this->getStringTranslationStub());
    
  3. Set our expectation.

    $expected =
      $this->t('You are logged in.') .
      '<br>' .
      $this->t('You are special.');
    $this->assertEquals($expected, $controller->getMessageForUser($this->user));
    
  4. At this point, let's run our tests again. We should see a failure:

    ---- Drupal\Tests\my_testing_module\Unit\Controller\MyMessageControllerTest ----
    
    
    Status    Group      Filename          Line Function                            
    --------------------------------------------------------------------------------
    Fail      Other      phpunit-259.xml      0 Drupal\Tests\my_testing_module\Unit
        PHPunit Test failed to complete; Error: PHPUnit 6.5.14 by Sebastian
        Bergmann and contributors.
        
        Testing
        Drupal\Tests\my_testing_module\Unit\Controller\MyMessageControllerTest
        .F                                                                  2 / 2
        (100%)
        
        Time: 233 ms, Memory: 6.00MB
    
        There was 1 failure:
        
        1)
        Drupal\Tests\my_testing_module\Unit\Controller\MyMessageControllerTest::testGetMessageForMySuperSecretPrivilegeIsCorrect
        Failed asserting that two strings are equal.
        --- Expected
        +++ Actual
        @@ @@
        -'You are logged in.<br>You are special.'
        +'You are logged in.'    
    
  5. From this we can see that we're missing the You are special. string. This is expected because we are just using the generic mock user. It has no special permissions. So we need to add those permissions. To do so, we simply need to stub the user's ::hasPermission().

    $map = [
      ['my super secret privilege', TRUE],
      ['yet another privilege', FALSE],
    ];
    $this->user->expects($this->any())
      ->method('hasPermission')
      ->will($this->returnValueMap($map));
    

    ^ This means that any time the hasPermission method is called on our mock user, it will return a value based on the defined map above. If the parameter passed in is 'my super secret privilege', it returns TRUE. If it is 'yet another privilege', it returns FALSE.

  6. Now if we rerun the tests, we should see two passing tests:

    ---- Drupal\Tests\my_testing_module\Unit\Controller\MyMessageControllerTest ----
    
    
    Status    Group      Filename          Line Function                            
    --------------------------------------------------------------------------------
    Pass      Other      MyMessageControll   71 Drupal\Tests\my_testing_module\Unit
        
    Pass      Other      MyMessageControll   84 Drupal\Tests\my_testing_module\Unit
    
  7. Now suppose instead of stubbing the user's ::hasPermission() method, we wanted to do a full-fledged mock. We could do so with a few minor changes:

    1. Change $this->any() to $this->exactly(2)
    2. Add ->withConsecutive() statement, with two rows where each permission is spelled out in the expected sequence

    This would end up looking like this:

     $this->user->expects($this->exactly(2))
       ->method('hasPermission')
       ->withConsecutive(
         [$this->equalTo('my super secret privilege')],
         [$this->equalTo('yet another privilege')],
       )
       ->will($this->returnValueMap($map));
    
  8. We can now repeat these steps for the other three test cases, which should go much quicker now that we've knocked the first one out.

Need Assistance?

If you're stuck, have a look at Pull Request #2