Seldom are tested classes so simple as our calculator class. Most of the time, they will have dependencies that in turn also have dependencies. So unit testing becomes a bit more complicated. In fact, the ease with which unit tests are written has become a litmus test for the quality of the code being tested—the less complicated the unit test, the better the code.
As our second example of writing unit tests, let's go into the "real world" and test one of the classes we wrote in this book, namely, the UserTypesAccess class. If you remember from Chapter 10, Access Control, we created this service to be used on routes as an access checker. Although we can write functional tests that verify that it works well as part of the access system, we can also write a unit test to check the actual code in the access() method. So let's get started.
The first thing we need to do is to create the class (respecting the directory placement as well as the class namespace):
namespace Drupal\Tests\user_types\Unit; use Drupal\Tests\UnitTestCase; /** * Tests the UserTypesAccess class methods. * * @group user_types */ class UserTypesAccessTest extends UnitTestCase {}
So far things look like our previous example—we have the PHPDoc information and we are extending the UnitTestCase class. So let's write a test for the access() method of the UserTypesAccess class. However, if you remember, this method takes two arguments (a user account and a route object) and also uses the entity type manager which is injected in the class. So that is where the bulk of our complication lies. What we need to test is the return value of the method depending on these arguments. Basically, whether it will allow or deny access if the user account has certain values found on the route.
In unit testing, dependencies are usually mocked. This means PHPUnit will create empty lookalike objects that behave as we describe them to and we can use these as the dependencies. The way to create a simple mock object is this:
$user = $this->createMock('Drupal\user\Entity\User');
The $user object will now be a mock of the Drupal 8 User entity class. It, of course, won't do anything but it can be used as a dependency. But to actually make it useful, we need to prescribe some behavior to it based on what the tested code does with it. For example, if it calls its id() method, we need to prescribe this behavior. We can do this with expectations:
$user->expects($this->any()) ->method('id') ->will($this->returnValue(1));
This tells the mock object that for every call to the id() method on it, it should return the value 1. The expects() method takes in a matcher which can be even more restrictive. For example, instead of $this->any(), we can use $this->once(), which means that the mock object can have its id() method called only once. Check out the base class for the other available options, as well as what you can pass to the will() method—although $this->returnValue() is going to be the most common one. Finally, if the id() method takes an argument, we can also have the with() method to which we pass the value of the expected argument in the matcher.
A more complex way of creating a mock is by using the mock builder:
$user = $this->getMockBuilder('Drupal\user\Entity\User') ->getMock();
This will get the same mock object but will allow for some more options in its construction. I recommend checking out the PHPUnit documentation for more information as this is as deep as we are going to go in this book on mocking objects.
Now that we know a bit about mocking, we can proceed with writing our test. To do this, we need to think about the end goal and work our way back to all the method calls we need to mock. Just as a reminder, this is the code that we need to test:
public function access(AccountInterface $account, Route $route) { $user_types = $route->getOption('_user_types'); if (!$user_types) { return AccessResult::forbidden(); } if ($account->isAnonymous()) { return AccessResult::forbidden(); } $user = $this->entityTypeManager->getStorage('user')->load($account->id()); $type = $user->get('field_user_type')->value; return in_array($type, $user_types) ? AccessResult::allowed() : AccessResult::forbidden(); }
So, at the first glance, we need to mock EntityTypeManager. The method arguments we will instantiate manually with some dummy data inside. However, mocking EntityTypeManager is going to be quite complicated. A call to its getStorage() method needs to return a UserStorage object. This needs to also be mocked because a call on its load() method needs to return a User entity object. Finally, we also need to mock that because a call to its get() method is also expected to return a value object.
As I mentioned, we will proceed by going back from our end goal. So we can start with instantiating the types of AccountInterface objects we want to pass, as well as the route objects:
/**
* Tests the UserTypesAccess::access() method.
*/
public function testAccess() {
// User accounts
$anonymous = new UserSession(['uid' => 0]);
$registered = new UserSession(['uid' => 2]);
// Route definitions.
$manager_route = new Route('/test_manager', [], [], ['_user_types' => ['manager']]);
$board_route = new Route('/test_board', [], [], ['_user_types' => ['board']]);
$none_route = new Route('/test_board');
}
And the new use statements at the top:
use Drupal\Core\Session\UserSession; use Symfony\Component\Routing\Route;
Basically, we want to test what happens for both types of users: anonymous and registered. When instantiating the UserSession objects (which implement AccountInterface), we pass in some data to be stored with it. In our case, we need the user uid because it will be requested by the tested code when checking whether the user is anonymous or not.
Then, we create three routes: one where managers should have access, one where board members should have access, and one where no one should have access (as indicated by the _user_types option on the route). Do check back to Chapter 10, Access Control, if you don't remember what this functionality is about.
Once this is done, it follows to instantiate our UserTypesAccess class, in view of calling its access() method with various combinations of our account and route objects:
$access = new UserTypesAccess($entity_type_manager);
And the new use statement at the top:
use Drupal\user_types\Access\UserTypesAccess;
However, we don't yet have an entity type manager so we need to mock it. Here is all the code we need to mock the entity type manager to work for our tested code (this goes before the code we wrote so far in this test):
// User entity mock. $type = new \stdClass(); $type->value = 'manager'; $user = $this->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); $user->expects($this->any()) ->method('get') ->will($this->returnValue($type)); // User storage mock $user_storage = $this->getMockBuilder('Drupal\user\UserStorage') ->disableOriginalConstructor() ->getMock(); $user_storage->expects($this->any()) ->method('load') ->will($this->returnValue($user)); // Entity type manager mock. $entity_type_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityTypeManager') ->disableOriginalConstructor() ->getMock(); $entity_type_manager->expects($this->any()) ->method('getStorage') ->will($this->returnValue($user_storage));
First of all, you will notice that the entity type manager is only mocked at the very end. We first need to start the call chain which ends with a User entity object field value. So the first block mocks the User entity object which expects any number of calls to its get() method to which it will always return a stdClass() object with the property value that equals to the manager string. This way we are mocking the entity field system accessor.
Now that we have the User entity mock, we can use it as the return value of the UserStorage mock's load() method. This, in turn, is the return value of the entity type manager mock's getStorage() method. So, all of the code we wrote means that we have mocked the following chain:
$this->entityTypeManager->getStorage('user')->load($account->id());
It doesn't really matter what we pass to the load() method as we will always have that one user entity that has the manager user type.
Now that everything is mocked, we can use the $access object we created earlier and make assertions based on calls to its access() method:
// Access denied due to lack of route option. $this->assertInstanceOf('Drupal\Core\Access\AccessResultForbidden', $access->access($registered, $none_route)); // Access denied due to user being anonymous on any of the routes $this->assertInstanceOf('Drupal\Core\Access\AccessResultForbidden', $access->access($anonymous, $manager_route)); $this->assertInstanceOf('Drupal\Core\Access\AccessResultForbidden', $access->access($anonymous, $board_route)); // Access denied due to user not having proper field value $this->assertInstanceOf('Drupal\Core\Access\AccessResultForbidden', $access->access($registered, $board_route)); // Access allowed due to user having the proper field value. $this->assertInstanceOf('Drupal\Core\Access\AccessResultAllowed', $access->access($registered, $manager_route));
The return value is always an object that implements an interface—either AccessResultAllowed or AccessResultForbidden, so that is what we need to assert. We are checking four different use cases:
- Access denied if there is no route option
- Access denied for anonymous users on any of the routes
- Access denied for registered users with the wrong user type
- Access allowed for registered users with the proper user type
So with this, we can run the test and should hopefully get a green result:
../vendor/bin/phpunit ../modules/custom/user_types/tests/src/Unit/UserTypesAccessTest.php
This is the basics of writing unit tests. There are a lot more types of assertions and you'll end up mocking quite a lot of dependencies in Drupal 8. But don't be put off by the slow pace encountered at first as things will become faster as you get more experience.