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:
- The user only has the
my super secret privilege
permission. - The user only has the
yet another privilege
permission. - The user has both of the permissions set.
- 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:
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 returnsFALSE
$this->logger
is a stub logging service- We're also calling
$this->setStringTranslation($this->getStringTranslationStub())
, which is a mechanism for mocking$this->t()
methods.
public function testTitleShowsCurrentUser()
- This tests
MyMessageController::title()
$controller
is instantiated using the test doubles defined insetUp()
- 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.'
- This tests
Writing the Unit Tests
-
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() { }
-
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());
-
Set our expectation.
$expected = $this->t('You are logged in.') . '<br>' . $this->t('You are special.'); $this->assertEquals($expected, $controller->getMessageForUser($this->user));
-
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.'
-
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 returnsTRUE
. If it is'yet another privilege'
, it returnsFALSE
. -
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
-
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:- Change
$this->any()
to$this->exactly(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));
- Change
-
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