Chapter 17: Automated Testing

Automated testing is a process in which we rely on special software to continuously run pre-defined tests that verify the integrity of our application. To this end, automated tests are collections of steps that cover the functionality of an application and compare triggered outcomes to expected ones.

Manual testing is a great way to ensure that a piece of written functionality works as expected. The main problem encountered by most adopters of this strategy, especially those who use it exclusively, is regression. Once a piece of functionality is tested, the only way that you can guarantee that regressions (or bugs) were not introduced by another piece of functionality is by retesting it. And as the application grows, this becomes impossible to handle. This is where automated tests come in.

Automated testing uses special software that has an API that allows us to automate the steps involved in testing functionality. This means that we can rely on machines to run these tests as many times as we want, and the only thing stopping us from having a fully working application is the lack of proper test coverage with well-defined tests.

There's a lot of different software available for performing such tests and it's usually geared toward specific types of automated testing. For example, Behat is a powerful PHP-based open source behavior testing framework that allows the scripting of tests that mirror quite closely what a manual tester would do—interact with the application through the browser and test its behavior.

There are other testing frameworks that go much lower in the level of their testing target. For example, the PHP industry standard tool PHPUnit is widely used for performing unit tests. This type of testing focuses on the actual code at the lowest possible level; it tests whether class methods work properly by verifying their output after providing them with different input. A strong argument in favor of this kind of testing is that it encourages better code architecture, which can be (partly) measured by the ease with which unit testing can be written for it.

We also have functional or integration tests, which fall somewhere in between these two examples. These go higher than the code level and enlist application subsystems in order to test more comprehensive sets of functionality, without necessarily considering browser behavior and user interaction.

It is not difficult to agree that a well-tested application features a combination of different testing methodologies. For example, testing the individual architectural units of an application does not guarantee that the entire subsystem works, just as testing only the subsystem does not guarantee that its individual components will work properly under all circumstances. Also, the same is true for certain subsystems that depend on user interaction—these require test coverage as well.

In this chapter, we will see how automated testing works in Drupal. More specifically, we will go through and explain all the testing methodologies available for us as module developers and provide examples for them with two tests each. By the end of this chapter, you'll be ready to write your own tests and be familiar enough with the code to further explore the testing capabilities available.

The main topics we will cover in this chapter are as follows:

  • Testing methodologies in Drupal 9
  • Registering tests
  • Getting familiar with Unit, Kernel, Functional, and FunctionalJavaScript tests

Testing methodologies in Drupal 9

Drupal's PHP tests are all run by PHPUnit, which covers more testing methodologies than just those mentioned earlier. So, let's see what these are.

Drupal 9 comes with the following types of PHP-level testing:

  • Unit: Low-level class testing with minimal dependencies (usually mocked)
  • Kernel: Functional testing with the kernel bootstrapped, access to the database, and only a few loaded modules
  • Functional: Functional testing with a bootstrapped Drupal instance, a few installed modules, and using a Mink-based browser emulator (Goutte driver)
  • Functional JavaScript: Functional testing using the Selenium driver for Mink, allowing the testing of JavaScript-powered functionality

As mentioned, all of these test suites are built on top of PHPUnit and are, consequently, run by it. Based on the namespace the test classes reside in, as well as the directory placement, Drupal can discover these tests and know what type they are.

In this chapter, we will see examples of all of them as we go about testing some of the functionality we've been writing in this book.

PHPUnit

Drupal uses PHPUnit as the testing framework for all types of tests. In this section, we will see how we can work with it to run tests.

Important note:

In your development environment (or wherever you want to run the tests), make sure you have the composer dependencies installed with the --dev flag. This will include PHPUnit. Remember not to do this in your production environment as you can compromise the security of your application.

Although Drupal has a UI for running tests, PHPUnit is not well integrated with this. So, it's recommended that we run the tests using the command line instead. Actually, it's very easy to do so. To run an entire test suite (of a certain type), we have to navigate to the Drupal core folder (this works in a normal Drupal site installation where the vendor folder is located there):

cd core  

And run the following command:

../vendor/bin/phpunit --testsuite=unit  

This command goes back a folder through the vendor directory and uses the installed phpunit executable.

If you are following along with the GitHub repository that accompanies this book, the vendor folder is placed elsewhere and there is a PHPUnit configuration file at the root of the project. So, to achieve the same as what we've done here, you would run from the root folder:

./vendor/bin/phpunit --testsuite=unit  

Going forward, we will assume a regular Drupal installation where the PHPUnit configuration file is located in the core folder.

As an option, in the previous example, we specified that we only want to run unit tests. Omitting that would run all types of tests. However, for most of the others, there will be some configuration needed, as we will see in the respective sections. If we want to run a specific test, we can pass it as an argument to the phpunit command (the path to the file):

../vendor/bin/phpunit tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php  

In this example, we run a Drupal core test that tests the UrlGenerator class. Alternatively, we can run multiple tests that belong to the same group (we will see how tests are added to a group soon):

../vendor/bin/phpunit --group=Routing

This runs all the tests from the Routing group, which actually contains the UrlGeneratorTest we saw earlier. We can run tests from multiple groups if we separate them with commas.

Also, to check what the available groups are, we can run the following command:

../vendor/bin/phpunit --list-groups

This will list all the groups that have been registered with PHPUnit.

Finally, we can also run a specific method found inside a test by using the –filter argument:

../vendor/bin/phpunit --filter=testAliasGenerationUsingInterfaceConstants  

This is one of the test methods from the same UrlGeneratorTest we saw before and is the only one that would run.

Registering tests

There are certain commonalities between the various test suite types regarding what we need to do in order for Drupal (and PHPUnit) to be able to discover and run them.

First, we have the directory where the test classes should go. The pattern is this: tests/src/[suite_type], where [suite_type] is the name of the test suite type this test should be. It can be one of the following:

  • Unit
  • Kernel
  • Functional
  • FunctionalJavascript

So, for example, unit tests would go inside the tests/src/Unit folder of our module.

Second, the test classes need to respect a namespace structure as well:

namespace DrupalTests[module_name][suite_type]  

This is also pretty straightforward to understand.

Third, there is a certain metadata that we need to have in the test class PHPDoc. Every class must have a summary line describing what the test class is for. Only classes that use the @coversDefaultClass attribute can omit the summary line. Moreover, all test classes must have the @group PHPDoc annotation indicating the group they are part of. This is how PHPUnit can run tests that belong to certain groups only.

So, now that we know how to register and run tests, let's look at unit tests and see how we can write our own.

Unit tests

As briefly mentioned at the beginning of the chapter, unit tests are used for testing single units that make up the code architecture. In practice, this means testing individual classes, especially the methods they contain and what they should be doing. Since the testing happens at such a low level, they are by far the fastest tests that can be run.

The logic behind unit tests is quite simple: after providing input, the test asserts that the method output is correct. Typically, the more input -> output scenarios it covers, the more stable the tested code is. For example, tests should also cover unexpected scenarios, as well as exercise all the code contained in the tested methods (such as forks created by if/else statements).

The programming pattern of dependency injection—objects should receive as dependencies other objects they might need—becomes critical when it comes to unit testing. The reason is that if class methods work with the global scope or instantiate other objects, we can no longer test them cleanly. Instead, if they require dependencies, we can mock them and pass them within the context of the executed tests. We will see some examples of this shortly. But before we do that, let's create a simple class that can be easily tested using a unit test.

A typical example is a simple calculator class. It will take two numbers as arguments to its constructor and have four methods for performing basic arithmetic on those numbers. We'll put this into our Hello World module:

namespace Drupalhello_world;

/**

* Class used to demonstrate a simple Unit test.

*/

class Calculator {

  protected $a;

  protected $b;

  public function __construct($a, $b) {

    $this->a = $a;

    $this->b = $b;

  }

  public function add() {

    return $this->a + $this->b;

  }

  public function subtract() {

    return $this->a - $this->b;

  }

  public function multiply() {

    return $this->a * $this->b;

  }

  public function divide() {

    return $this->a / $this->b;

  }

}

Nothing very complicated here. You could argue that a calculator class should not get any dependencies but instead pass the numbers to the actual arithmetic methods. However, this will work just fine for our example and is a bit less repetitive.

Now, let's create the first unit test to make sure that this class behaves as we expect it to. In the previous section, we saw which directory these need to go in. So, in our case, it will be /tests/src/Unit. And the test class looks like this:

namespace DrupalTestshello_worldUnit;

use Drupalhello_worldCalculator;

use DrupalTestsUnitTestCase;

/**

* Tests the Calculator class methods.

*

* @group hello_world

*/

class CalculatorTest extends UnitTestCase {

  /**

   * Tests the Calculator::add() method.

   */

  public function testAdd() {

    $calculator = new Calculator(10, 5);

    $this->assertEquals(15, $calculator->add());

  }

  /**

   * Tests the Calculator::subtract() method.

   */

  public function testSubtract() {

    $calculator = new Calculator(10, 5);

    $this->assertEquals(5, $calculator->subtract());

  }

  /**

   * Tests the Calculator::multiply() method.

   */

  public function testMultiply() {

    $calculator = new Calculator(10, 5);

    $this->assertEquals(50, $calculator->multiply());

  }

  /**

   * Tests the Calculator::divide() method.

   */

  public function testDivide() {

    $calculator = new Calculator(10, 5);

    $this->assertEquals(2, $calculator->divide());

  }

}  

First of all, you notice that the namespace corresponds to the pattern what we saw in the previous section. Second of all, the PHPDoc contains the required information: a summary and the @group tag. Third of all, the class name ends with the word Test. Finally, the class extends UnitTestCase, which is the base class we need to extend for all unit tests.

Important note:

All types of test class names in Drupal need to end with the word Test and extend the relevant base class that provides specific code for that type of test.

Then, we have the actual methods that test various aspects of the Calculator class and which always have to start with the word test. This is what tells PHPUnit that they need to be run. These methods are the actual standalone tests themselves, meaning that the CalculatorTest class has four tests. Moreover, each of these tests runs independently of the others.

Since the Calculator arithmetic is very simple, it's not difficult to understand what we are doing to test it. For each method, we are instantiating a new instance with some numbers, and then we assert that the result from the arithmetic operation is equal to what we expect. The base class provides a multitude of different assertion methods that we can use in our tests. Since there are so many of them, we are not going to cover them all here. We will see more as we write more tests, but I strongly recommend that you check the base classes of the various types of test suites for methods that start with the word assert. A great way to do this is also to use an IDE that autocompletes as you type the method name. It can be very handy.

With this, we can already run the test and see whether it passes. Normally, it should because we can do math in our heads and we know it's correct:

../vendor/bin/phpunit ../modules/custom/hello_world/tests/src/Unit/CalculatorTest.php  

The result should be green:

OK (4 tests, 4 assertions)  

However, earlier I mentioned that a good test also accounts for unexpected situations and negative responses. We have not done so very well in our example. If we look at testAdd(), we can see that the assertion is correct with those two numbers. But what if we later go to the Calculator::add() method and change it to this by accident:

return 15;  

The test will still pass, but will it actually be a true positive? Not really, because if we pass different numbers, the calculation won't match anymore. So, we should test these methods with more than just one set of numbers to actually prove that the math behind the Calculator class is valid.

So, instead, we can do something like this:

$calculator = new Calculator(10, 5);

$this->assertEquals(15, $calculator->add());

$calculator = new Calculator(10, 6);

$this->assertEquals(16, $calculator->add());  

This way, we are sure that the addition operation works correctly. One trade-off in this is that we have a bit of repetitive code, especially if we have to do this for all the other operations as well.

Generally, when writing tests, repetition is much more acceptable than when writing the actual code. Many times, there is nothing you can do about it as the code will seem very repetitive. However, in our case, we can actually do something by using the setUp() method, which is called by PHPUnit before each test method runs. Its purpose is to perform various preparation tasks that are common for all the tests in the class. However, don't take this to mean that it runs only once and then is used by all. In fact, it runs before each individual test method.

So, what we can do is something like this:

/**

* @var Drupalhello_worldCalculator

*/

protected $calculatorOne;

/**

* @var Drupalhello_worldCalculator

*/

protected $calculatorTwo;

/**

* {@inheritdoc}

*/

protected function setUp() {

  parent::setUp();

  $this->calculatorOne = new Calculator(10, 5);

  $this->calculatorTwo = new Calculator(10, 2);

}  

We create two class properties and inside the setUp() method, we assign to them our calculator objects. A very important thing to keep in mind is to always call the parent of this method because it does very important things for the environment setup, especially as we move to Kernel and Functional tests.

Now, the testAdd() method can look like this:

public function testAdd() {

  $this->assertEquals(15, $this->calculatorOne->add());

  $this->assertEquals(12, $this->calculatorTwo->add());

}  

Much cleaner and less repetitive. Based on this, you can extrapolate and apply the same changes to the other methods yourself.

Mocked dependencies

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 DrupalTestsuser_typesUnit;

use DrupalTestsUnitTestCase;

/**

* 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 the complexity 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 prescribe them to and we can use these as the dependencies. The way to create a simple mock object is this:

$user = $this->createMock('DrupaluserEntityUser');  

The $user object will now be a mock of the Drupal User entity class. It, of course, won't do anything, but it can be used as a dependency. To actually make it useful, we need to prescribe some behavior for 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('DrupaluserEntityUser')

  ->getMock();

This will get the same mock object but will allow 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 first glance, we see that 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 to 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 DrupalCoreSessionUserSession;

use SymfonyComponentRoutingRoute;  

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 refer back to Chapter 10, Access Control, if you don't remember what this functionality is about.

Once this is done, we instantiate our UserTypesAccess class, with a view to 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 Drupaluser_typesAccessUserTypesAccess;

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('DrupaluserEntityUser')

  ->disableOriginalConstructor()

  ->getMock();

$user->expects($this->any())

  ->method('get')

  ->will($this->returnValue($type));

// User storage mock

$user_storage = $this->getMockBuilder('DrupaluserUserStorage')

  ->disableOriginalConstructor()

  ->getMock();

$user_storage->expects($this->any())

  ->method('load')

  ->will($this->returnValue($user));

// Entity type manager mock.

$entity_type_manager = $this->getMockBuilder('DrupalCoreEntityEntityTypeManager')

  ->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 is equal to the manager string. This way, we are mocking the entity field system accessor.

Important Note

While using the mock builder to create our mocks, we can use the disableOriginalConstructor() method to prevent PHPUnit from calling the constructor of the original class. This is important in order to prevent the need for all sorts of other dependencies that don't actually impact the tested code.

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(DrupalCoreAccessAccessResultForbidden::class, $access->access($registered, $none_route));

// Access denied due to user being anonymous on any of the routes

$this->assertInstanceOf(DrupalCoreAccessAccessResultForbidden::class, $access->access($anonymous, $manager_route));

$this->assertInstanceOf(DrupalCoreAccessAccessResultForbidden::class, $access->access($anonymous, $board_route));

// Access denied due to user not having proper field value

$this->assertInstanceOf(DrupalCoreAccessAccessResultForbidden::class, $access->access($registered, $board_route));

// Access allowed due to user having the proper field value.

$this->assertInstanceOf(DrupalCoreAccessAccessResultAllowed::class, $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 many more types of assertions and you'll end up mocking quite a lot of dependencies in Drupal. But don't be put off by the slow pace encountered at first as things will become faster as you get more experience.

Kernel tests

Kernel tests are the immediate higher-level testing methodology we can have in Drupal and are actually integration tests that focus on testing various components. They are faster than regular Functional tests as they don't do a full Drupal install, but use an in-memory pseudo installation that is much faster to bootstrap. For this reason, they also don't handle any browser interactions and don't install any modules automatically.

Apart from the code itself, Kernel tests also work with the database and allow us to load the modules that we need for running the test. However, unlike the Functional tests we will see next, Kernel tests also require us to manually trigger the installation of any database schemas we need. But we will see how we can do this in the two examples we cover in this section.

Before we can work with Kernel tests, though, we need to make sure we have a connection to the database, and PHPUnit is aware of this. Inside the core folder of our Drupal installation, we find a phpunit.xml.dist file, which we need to duplicate and rename to phpunit.xml. This is the PHPUnit configuration file. Normally, this file should already be ignored by Git, so no need to worry about committing it to the repository.

In this file, we find an environment variable called SIMPLETEST_DB where we can specify the connection to the database, using the format shown in the following commented code:

mysql://username:password@localhost/databasename#table_prefix  

Once that is in, PHPUnit will be able to connect to the database in order to install Drupal for Kernel tests as well as Functional and FunctionalJavaScript tests.

If you are following along with the GitHub repository accompanying this book, this configuration file is found in the root folder and is already prepared for running all the types of tests we cover in this book.

Important Note:

As a rule of thumb, you should always opt for Kernel tests over Functional tests whenever browser interactions are not involved and Kernel tests are enough to do the job. This is because a suite full of tests can end up taking a long time to run so you should make it as performant as possible.

TeamCleaner test

Now that we have that covered, it's time to write our first Kernel test. And a nice simple example can be to test the TeamCleaner queue worker plugin we created in Chapter 14, Batches, Queues, and Cron. If you are wondering why this cannot be tested using the ultra-fast unit testing methodology, the answer is that its single method doesn't return anything. Instead, it alters database values that we need to access in order to check it happened correctly.

The test class goes naturally in the tests/src/Kernel folder of our module and can start off like this:

namespace DrupalTestssportsKernel;

use DrupalKernelTestsKernelTestBase;

/**

* Test the TeamCleaner QueueWorker plugin.

*

* @group sports

*/

class TeamCleanerTest extends KernelTestBase {}  

The namespace is consistent with the ones we've seen so far and we have the correct PHPDoc annotations to register the test. Moreover, this time, we are extending from KernelTestBase.

The first thing we need to do is specify which modules we want loaded when running this test. For our case, this is the sports module, so we can add a class property that contains this name:

/**

* {@inheritdoc}

*/

protected static $modules = ['sports'];  

Specifying a list of modules here does not actually install them but simply loads and adds them to the service container. So yes, we have access to the module and code as well as the container. But that also means that schemas defined by these modules are not actually created, so we need to do that manually. The same is true for the configuration the module is shipped with. But we can handle these things in the setUp() method or in the actual test method itself. We'll opt for the latter because, in this case, we only have one test method in the class. And the whole thing can look like this:

/**

* Tests the TeamCleaner::processItem() method.

*/

public function testProcessItem() {

  $this->installSchema('sports', 'teams');

  $database = $this->container->get('database');

  $fields = ['name' => 'Team name'];

  $id = $database->insert('teams')

    ->fields($fields)

    ->execute();

  $records = $database->query("SELECT id FROM {teams} WHERE id   = :id", [':id' => $id])->fetchAll();

  $this->assertNotEmpty($records);

  $worker = new TeamCleaner([], NULL, NULL, $database);

  $data = new stdClass();

  $data->id = $id;

  $worker->processItem($data);

  $records = $database->query("SELECT id FROM {teams} WHERE id   = :id", [':id' => $id])->fetchAll();

  $this->assertEmpty($records);

}  

And the use statement:

use DrupalsportsPluginQueueWorkerTeamCleaner;  

Since the TeamCleaner plugin removes teams, it's enough to only install that table. We can do that using the parent installSchema() method, to which we pass the module name and table we want installed. We don't actually deal with players, so we should avoid doing unnecessary work, such as the creation of the players table.

Then, very similar to how we do it in real code, we get the database service from the container and add a record to the teams table. This will be the test record that we delete, so we remember its $id. But before we test this, we want to make absolutely sure that our record got saved. So we query for it and assert that the result is not empty. The assertNotEmpty() method is another helpful assertion that we can use when dealing with arrays.

Now that we are certain the record is in the database, we can "process" it using our plugin. So, we instantiate a TeamCleaner object, passing all its required dependencies—most importantly, the database service. Then, we create a simple object that mimics what the processItem() method expects and calls the latter while passing the former to it. At this point, if our plugin did its job correctly, the team record should have been deleted from the database. So, we can query for it and this time assert the opposite of what we did before: that the query comes back empty.

And with this, our test is finished. As always, we should actually run it and make sure it passes:

../vendor/bin/phpunit ../modules/custom/sports/tests/src/Kernel/TeamCleanerTest.php  

And that is a very simple example of using Kernel tests for testing a component, particularly one that integrates with the database. We could have used a Functional test as well but that would have been overkill—it would run slower and make no use of the benefits that it offers over Kernel testing, such as browser integration.

Note

We went with an almost Unit-like approach here by manually instantiating the TeamCleaner plugin class and passing dummy data to it. The cool thing about Kernel tests is that we could have even used the worker plugin manager and instantiated the plugin with that instead.

CsvImporter test

After this simple example, let's write another test that illustrates a more complex scenario. And we will write one that tests the CsvImporter plugin we created in the previous chapter.

There is quite a lot of functionality that goes into this plugin and working with it—we have the actual importing, the plugin and configuration entity creation, the user interface for doing so, and so on. It's a very good example of functionality that can benefit from multi-methodology test coverage. In this respect, we start with testing its underlying purpose, that of the product import, for which we don't need browser interactions. This means that we can use a Kernel test.

Similar to how we wrote the previous test, we can start with the class like so (this time in the products module):

namespace DrupalTestsproductsKernel;

use DrupalKernelTestsKernelTestBase;

/**

* Tests the CSV Product Importer

*

* @group products

*/

class CsvImporterTest extends KernelTestBase {}  

Nothing new so far.

Next, we need to specify the modules we need loaded. And here we have a bigger list:

/**

* {@inheritdoc}

*/

protected static $modules = ['system', 'csv_importer_test', 'products', 'image', 'file', 'user'];  

Only the products module may seem obvious to you at this point, but all the rest are also needed. The system, image, file and user modules are all somehow needed for dealing with the file upload and storage process that is needed for the CsvImporter plugin.

Important Note:

It's not always so easy to figure out which modules are needed, so it will involve a bit of a trial-and-error process, at least in the beginning. A typical scenario is to run the test and notice failures due to missing functionality. Tracking this functionality to a module and specifying this module in the list is how you usually end up with a complete module list, especially when the test is complex and needs a wide range of subsystems with dependencies.

But you may be wondering what's with the csv_importer_test module there. Often, you may need to create modules used only for the tests—usually because they contain some configuration you want to use in your testing. In our case, we did so to demonstrate where these modules would go and to add a products.csv test file that we can use in our tests.

Tests modules go inside the tests/modules folder of the module that contains the tests that use them. So, in our case, we have csv_importer_test with its info.yml file:

name: CSV Importer Test

description: Used for testing the CSV Importer

core_version_requirement: ^9

type: module

package: Testing  

And the mentioned CSV file we will use is right next to it:

id,name,product_number

1,Car,45345

2,Motorbike,54534  

Now that we have covered that, we can write the test method:

/**

* Tests the import of the CSV based plugin.

*/

public function testImport() {

  $this->installEntitySchema('product');

  $this->installEntitySchema('file');

  $this->installSchema('file', 'file_usage');

  $entity_type_manager = $this->container->get('entity_type.  manager');

  // Assert we have no products in the system.

  $products = $entity_type_manager->getStorage('product')->loadMultiple();

  $this->assertEmpty($products);

  $csv_path = drupal_get_path('module', 'csv_importer_test') . '/products.csv';

  $csv_contents = file_get_contents($csv_path);

  $file = file_save_data($csv_contents, 'public://simpletest-  products.csv', FileSystemInterface::EXISTS_REPLACE);

  $config = $entity_type_manager->getStorage('importer')-  >create([

    'id' => 'csv',

    'label' => 'CSV',

    'plugin' => 'csv',

    'plugin_configuration' => [

      'file' => [$file->id()]

    ],

    'source' => 'Testing',

    'bundle' => 'goods',

    'update_existing' => true

  ]);

  $config->save();

  $plugin = $this->container->get('products.importer_manager')-  >createInstanceFromConfig('csv');

  $plugin->import();

  $products = $entity_type_manager->getStorage('product')-  >loadMultiple();

  $this->assertCount(2, $products);

  $products = $entity_type_manager->getStorage('product')-  >loadByProperties(['number' => 45345]);

  $this->assertNotEmpty($products);

  $this->assertCount(1, $products);

}  

And the use statement at the top:

use DrupalCoreFileFileSystemInterface;  

The initial setup here is a bit more complicated, partly because of Kernel tests not installing module schemas. Using the parent installEntitySchema() method, we can install all the necessary tables for the Product and File content entities. However, since we are working with managed files, we also need to install the file_usage table manually. It is not technically an entity table. Again, there is no shame in arriving at these steps using trial and error.

Now that we have the basics set up, we can do a sanity check and ensure that we don't have any product entities in the database. There is no reason why we should have any, but it doesn't hurt to ensure it. This guarantees a valid test since our goal will be to later assert the existence of products.

Then we create a managed File entity by using the products.csv file from the csv_importer_test module. The drupal_get_path() function is a very common way of retrieving the relative path to a module or a theme, regardless of where it is actually located. And we save the contents of this file into the public:// filesystem of the testing environment. Keep in mind, though, that after the test runs successfully, this file gets removed as Drupal cleans up after itself.

Next, we need to create an Importer configuration entity that uses the CSV-based plugin to run the import. And instead of doing it through the UI, we do it programmatically. Using the storage handler, we create the entity as we learned in Chapter 6, Data Modeling and Storage. Once we have that, we use the Importer plugin manager to create an instance based on this configuration entity (to which we gave the ID csv). And finally, we run the import of the products.

Now, for the assertions, we do a double-check. Since our test CSV contains two rows, we load all the product entities again and assert that we have a total of two. No more, no less. And here we see another useful assertion method for working with arrays: assertCount(). But then we get a bit more specific and try to load a product that has a field value (the number) equal to an expected number from the test CSV file. And assert that it is, in fact, found as well.

We could even do some more assertions. For example, we can check that all the Product field values have been set correctly. I'll let you explore ways in which you can do this—either by querying based on these values or asserting equality between field values and their expected ones. But it's important to not go overboard as it will impact speed and, in some cases, add insufficient value to the test coverage to compensate for it. The trick is to find the right balance.

Finally, with our test in place, we can actually run it:

../vendor/bin/phpunit ../modules/custom/products/tests/src/Kernel/CsvImporterTest.php  

And this test should pass as well.

In the previous section, we looked at Kernel tests and said that they are basically integration tests that focus on components rather than interactions with the browser. In the next section, we'll go one level up and talk about fully fledged functional tests, otherwise called browser tests (due to the name of the base class we need to extend).

Functional tests

Functional tests in Drupal use a simulated browser (using the popular Mink emulator) that allows users to click links, navigate to pages, work with forms, and make assertions regarding HTML elements on the page. What they don't allow is testing JavaScript-based interactions (see the next section for those).

Functional tests extend the DrupalTestsBrowserTestBase class, which is integrated with PHPUnit like the ones we've seen before. The base class contains loads of methods both for asserting things and for shortcuts to perform Drupal (and web)-related tasks: creating users, entities, navigating to pages, filling in and submitting forms, logging in, and so on. And just like before, each test (class method) runs in isolation, so things such as content and users cannot be shared across multiple tests but would have to be recreated (perhaps using the setUp() method as we've already seen).

Browser tests perform a full Drupal installation with a minimal number of modules (using the Testing installation profile). This means that we can specify to install other modules as well, and the schema for these also gets installed. Moreover, it's also important to understand that the resulting installation has got nothing in common with our current development site. Any configuration we need, we have to create. There are no users, no content and no files. So, it is a brand new, parallel installation that runs for the duration of one single test and gets cleaned up as it finishes.

Configuration for Functional tests

Before writing our Functional tests, we need to turn back to our phpunit.xml file and change some environment variables. Apart from the SIMPLETEST_DB variable we adjusted earlier, we also have the SIMPLETEST_BASE_URL and BROWSERTEST_OUTPUT_DIRECTORY. The first is used to know where the application can be accessed in the browser. The latter is the directory where output data can be saved by PHPUnit and needs to be an absolute local path (for example, a folder in the local files folder):

/var/www/sites/default/files/browser-output   

Moreover, make sure the user running the test has permissions to write into the sites/simpletest folder as that is where the virtual filesystem is created for each test. The easiest way to do it is to change the folder ownership to the web server user that runs the process. In the case of Apache, this is usually www-data.

Hello World page test

The first Functional test we will write is for the Hello World page we created and the functionality behind it. We will test whether the page shows the correct Hello World message, also depending on the value found in the configuration. So, let's create the class for it, naturally in the hello_world module, inside the tests/src/Functional folder:

namespace DrupalTestshello_worldFunctional;

use DrupalTestsBrowserTestBase;

/**

* Basic testing of the main Hello World page.

*

* @group hello_world

*/

class HelloWorldPageTest extends BrowserTestBase {}  

You can really see the consistency with the other types of tests. But in this case, as mentioned, we extend from BrowserTestBase.

Also, like before, we can configure a number of modules we want installed:

/**

* {@inheritdoc}

*/

protected static $modules = ['hello_world', 'user'];

We will need the User module for the second test we run, which will go in the same class as this one.

Additionally, we also need to tell the test what Drupal theme it should use:

/**

* {@inheritdoc}

*/

protected $defaultTheme = 'stable';

We go with Stable, which contains core markup that we can assert, or we could have also used Stark, which doesn't. The choice is yours.

But let's proceed with the first, easier test:

/**

* Tests the main Hello World page.

*/

public function testPage() {

  $expected = $this->assertDefaultSalutation();

  $config = $this->config('hello_world.custom_salutation');

  $config->set('salutation', 'Testing salutation');

  $config->save();

  $this->drupalGet('/hello');

  $this->assertSession()->pageTextNotContains($expected);

  $expected = 'Testing salutation';

  $this->assertSession()->pageTextContains($expected);

}  

If you remember, our /hello page shows a greeting depending on the time of day, unless an administrator has overridden that message through a configuration form. So, we start this test by asserting that with a fresh install that has no override, we see the time-based greeting. And for that, we create a separate assertion message since it's a bit wordy and we will reuse it:

/**

* Helper function to assert that the default salutation is present on the page.

*

* Returns the message so we can reuse it in multiple places.

*/

protected function assertDefaultSalutation() {

  $this->drupalGet('/hello');

  $this->assertSession()->pageTextContains('Our first route');

  $time = new DateTime();

  $expected = '';

  if ((int) $time->format('G') >= 00 && (int) $time-  >format('G') < 12) {

    $expected = 'Good morning';

  }

  if ((int) $time->format('G') >= 12 && (int) $time-  >format('G') < 18) {

    $expected = 'Good afternoon';

  }

  if ((int) $time->format('G') >= 18) {

    $expected = 'Good evening';

  }

  $expected .= ' world';

  $this->assertSession()->pageTextContains($expected);

  return $expected;

}

The very first thing we do here is use the drupalGet() method to navigate to a path on the site. Do check out the method signature for all the options you can pass to it. And the first assertion we make is that the page contains the text Our first route (which is the page title). The parent assertSession() method returns an instance of WebAssert, which contains all sorts of methods for asserting the presence of elements on the current page in the Mink session. One such method is the generic pageTextContains() with which we simply check that the given text can be found anywhere on the page.

Although in quite a lot of cases asserting the presence of a text string is enough, you may want to ensure that it is actually the right one (to avoid false positives). For example, in our case, we could check that it is really the page title that is rendered inside an <h1> tag. We can do it like so:

$this->assertSession()->elementTextContains('css', 'h1', 'Our first route');  

The elementTextContains() method can be used to find an element on the page based on a locator (CSS selector or xpath) and assert that it contains the specified text. In our example, we use the CSS selector locator and we try to find the <h1> element.

If all of that is okay, we proceed with asserting that the actual salutation message is present on the page. Unfortunately, we have to duplicate quite some code because it is dependent on the time of day.

Going back to our actual test method, we can proceed knowing that the message is showing correctly on the page. And the next thing we want to test is the following: if there is a hello_world.custom_salutation configuration object with a salutation value, that is what should be shown. So, we programmatically create it. Next, we again navigate to the same path (we essentially reload the page) and check that the old message is not shown anymore and that the new one is instead.

So, if we actually run this test:

../vendor/bin/phpunit ../modules/custom/hello_world/tests/src/Functional/HelloWorldPageTest.php  

...darn. We get an error:

BehatMinkExceptionResponseTextException: The text "Good evening world" appears in the text of this page, but it should not.

It's as if we didn't even override the salutation message. But we did.

The problem is caching. Keep in mind, we are navigating these pages as anonymous users and caching is enabled on the site like in normal scenarios. In Chapter 11, Caching, I made a note about this particular problem—the max-age property only bubbles up to the page level for the dynamic page cache (logged-in users) and not for anonymous users.

Important Note:

This is a great example of automated testing shedding light on mistakes we introduce while developing and that we don't notice. We most likely wrote our functionality while having caching disabled and/or always visiting the page as a logged-in user. It's an easy mistake to make. Luckily, automated testing comes to the rescue.

The solution to this problem can be found using an all-out cache kill switch. This means that we need to alter a bit our logic to tell Drupal to never cache the pages where our salutation component is shown. This is the price we have to pay for the highly dynamic nature of our functionality and it's always a good exercise to evaluate whether it is worth it. There are, of course, much more complicated (and creative) solutions for this, but we will opt for the simple kill switch.

The kill switch is actually easy to use. It's a service called page_cache_kill_switch that we need to inject into our HelloWorldSalutation service. By now you should know how to do that so I won't repeat it here.

Next, at the beginning of the getSalutation() and getSalutationComponent() methods, we simply have to add this line:

$this->killSwitch->trigger();  

And now if we run this test, we should get a green result.

Hello World form test

The second Functional test we will write should test the salutation override form itself. In the previous one, we interacted with the configuration API directly to make changes to the configuration value. Now we will see whether the form to do so actually works. But since we can reuse quite a lot from the previous test, and they are very closely related, we can add it to the same class:

/**

* Tests that the configuration form for overriding the message    works.

*/

public function testSalutationOverrideForm() {

  $expected = $this->assertDefaultSalutation();

  $this->drupalGet('/admin/config/salutation-configuration');

  $this->assertSession()->statusCodeEquals(403);

  $account = $this->drupalCreateUser(['administer site   configuration']);

  $this->drupalLogin($account);

  $this->drupalGet('/admin/config/salutation-configuration');

  $this->assertSession()->statusCodeEquals(200);

  $this->assertSession()->pageTextContains('Salutation   configuration');

  $this->assertSession()->elementExists('css', '#edit-  salutation');

  $edit = [

    'salutation' => 'My custom salutation',

  ];

  $this->drupalPostForm(NULL, $edit, 'op');

  $this->assertSession()->pageTextContains('The configuration   options have been saved');

  $this->drupalGet('/hello');

  $this->assertSession()->pageTextNotContains($expected);

  $this->assertSession()->pageTextContains('My custom   salutation');

}  

We start this test in the same way, asserting that the hour-dependent message is shown. This also proves that each test runs in its own independent environment and changes to the configuration in one test have no impact on the other. They all start with a blank slate.

Then we navigate to the configuration form page and assert that we do not have access. For this, we use the statusCodeEquals() assertion method to check the response code. This is good because we need to be logged in with a user that has a certain permission.

Note

The access restrictions on the configuration form allow any user that has a certain permission. For this reason, our test should focus on that permission rather than something else that may indirectly include this permission. For example, it should not assume that a user with the administrator role has that permission.

So we create a new user account using the handy drupalCreateUser() method, whose first parameter is an array of permissions the user should have. We can then use the resulting User entity with the drupalLogin() method to log in. Under the hood, this navigates to the user login page, submits the form, and then asserts that everything went well. Now we can go back to the configuration form page and should have access— something that we also assert. In addition, we assert that we have the page title and that we have the salutation text field HTML element on the page. We do so using the elementExists() method, using the CSS selector. Again, check out WebAssert for all sorts of assertion methods that help you identify things on the page.

Now it's time to submit the form and override the salutation message. And we do this with drupalPostForm(), whose most important parameter is an array of values to fill in the form elements, keyed by the name parameter of the individual form HTML element. In our case, we only have one. Do check out the documentation of this method for more information on all the things you can do with it. Once the form is submitted, the page will reload and we can assert the presence of the confirmation message. And finally, we can go back to the /hello path and assert that the old message is no longer showing but the new overridden one does so instead.

Running the test class again should now include this new test as well and everything should be green. In the next section, we'll bring JavaScript into the picture so that we can also test the more dynamic browser integrations. But already you can notice that Kernel tests are much faster to run if you don't need to interact with a browser.

Functional JavaScript tests

The last type of PHP tests we can write in Drupal is the JavaScript-powered Functional test. FunctionalJavascript tests are useful when we want to test more dynamic client-side functionality such as JavaScript behaviors or Ajax interactions.

They are an extension of the regular Functional tests, but which use WebDriver. The latter is an API that allows things like Selenium to control browsers such as Chrome or Firefox. Drupal uses Chrome for this so make sure you have Selenium installed and working with the Chrome driver. We won't cover this here because it depends on your local environment and the current latest versions. But if you are following along with the GitHub repository accompanying this book, you should be all set up.

Assuming you have Selenium running, we can write some tests. But only after we add another environment variable to the PHPUnit configuration file (ensure the Selenium endpoint is correct for you):

<env name="MINK_DRIVER_ARGS_WEBDRIVER" value='["chrome", null, "http://localhost:4444/wd/hub"]'/>

Time test

If you remember from Chapter 12, JavaScript and Ajax API, we added to our Hello World salutation component a little time widget that displays the current hour in real time if the salutation is not overridden. This component is powered by JavaScript, and more importantly, appended to the page using JavaScript.

Moreover, in the previous section, we wrote a Functional test for the Hello World page in which we asserted the presence of the salutation message. However, the actual time widget would never show up there because the Mink driver used in these types of tests do not support JavaScript. So if we want to test that, we need to write a FunctionalJavascript test.

As expected, these types of tests follow the same patterns for the directory placement and namespaces. So our first test class can start like this:

namespace DrupalTestshello_worldFunctionalJavascript;

use DrupalFunctionalJavascriptTestsWebDriverTestBase;

/**

* Testing the simple Javascript timer on the Hello World page.

*

* @group hello_world

*/

class TimeTest extends WebDriverTestBase {}  

By now most of the above code should be clear. However, the base class we extend this time is the WebDriverTestBase class, which itself is a child of BrowserTestBase. Interestingly, it doesn't actually add much to the mix apart from configuring the test to use Selenium Web Driver and adding a few JavaScript specific helper methods. This is to demonstrate that most of the difference between Functional and FunctionalJavascript tests is determined by the actual Mink driver.

One extremely handy addition, though, is the ability to take screenshots. Many times when testing frontend interactions, things don't go as we thought and we don't understand why. The parent createScreenshot() method allows us to save a full page screenshot at any given moment that we can investigate for debugging purposes. All we have to do is pass in the name of the file we want to be saved. So do check that out.

Moving on with our test, let's add the modules we want to be enabled:

/**

* {@inheritdoc}

*/

protected static $modules = ['hello_world'];

As expected, the Hello World module is enough.

And the theme to use in the test installation:

/**

* {@inheritdoc}

*/

protected $defaultTheme = 'stable';

Now the very simple test method can look like this:

/**

* Tests the time component.

*/

public function testSalutationTime() {

  $this->drupalGet('/hello');

  $this->assertSession()->pageTextContains('The time is');

  $config = $this->config('hello_world.custom_salutation');

  $config->set('salutation', 'Testing salutation');

  $config->save();

  $this->drupalGet('/hello');

  $this->assertSession()->pageTextNotContains('The time is');

}  

We are using the exact same assertion techniques as before, but because JavaScript is enabled, the time widget text should show up now. And like before, we also test that if the salutation method is overridden, the time widget does not show up.

CsvImporter test

When learning about Kernel tests, we wrote a test for the CsvImporter that focused on the importing functionality given an existing Importer configuration entity (which we created programmatically). However, another important angle of this functionality is the process of creating this configuration entity as we are relying on Ajax to dynamically inject form elements related to the selected Importer plugin. So let's write a test for that as well.

Just as before, the test class can start with something like this:

namespace DrupalTestsproductsFunctionalJavascript;

use DrupalFunctionalJavascriptTestsWebDriverTestBase;

/**

* Testing the creation/edit of Importer configuration entities    using the CSV importer

*

* @group products

*/

class ImporterFormTest extends WebDriverTestBase {}  

Let's set the default theme:

/**

* {@inheritdoc}

*/

protected $defaultTheme = 'stable';

And like always, let's enable some modules:

/**

* {@inheritdoc}

*/

protected static $modules = ['image', 'file', 'node', 'products', 'csv_importer_test'];

If we try to run a test with these modules, it will fail because the Products module will be enabled before the Image one. And, if you remember, we created an Image field on the Product entity, but we forgot to make the image module a dependency on it. So let's add this quickly to the products.info.yml file:

dependencies:

  - drupal:image

Important Note:

The Node module is enabled because it defines the access content permission, which is used by the core machine_name form element. And this element is used on the Importer entity form so we'll need it in order for the tests to actually work.

Even though we only write one test method, there is quite a bit of preparation for it that we might want to reuse elsewhere. Plus, it also looks cleaner to be separated from the actual test method. So we can add it a setUp() method instead:

/**

* {@inheritdoc}

*/

protected function setUp() {

  parent::setUp();

  chmod('public://', 0777);

  $csv_path = drupal_get_path('module', 'csv_importer_test') .   '/products.csv';

  $csv_contents = file_get_contents($csv_path);

  $this->file = file_save_data($csv_contents, 'public://  simpletest-products.csv', FileSystemInterface::EXISTS_  REPLACE);

  $this->admin = $this->drupalCreateUser(['administer site   configuration']);

  ProductType::create(['id' => 'goods', 'label' => 'Goods'])-  >save();

}  

And the new use statements:

use DrupalproductsEntityProductType;

use DrupalCoreFileFileSystemInterface;  

As expected, the first thing we do is the same thing as we did in the previous test—load the test CSV file from the csv_importer_test module and "upload" it to Drupal creating a new managed File entity. But before that, we set permissions to allow the files to be uploaded to the test site public folder. Depending on your testing setup, this may not be needed.

Then, we create an administrator user account that has the permission needed for creating Importer configuration entities, as well as a bundle for the Product entity so that we can actually create products. We didn't need to worry about the bundle in the previous test because we created the Importer configuration programmatically. But now, through the UI, a bundle needs to exist in order to select it.

The resulting File entity and admin user account we store on class properties, so we should also define those:

/**

* @var DrupalfileFileInterface

*/

protected $file;

/**

* @var DrupalCoreSessionAccountInterface

*/

protected $admin;

And with this we are ready to write our empty test method and start filling it up step by step:

/**

* Tests the importer form.

*/

public function testImporterForm() {}

We can start with the basics:

$this->drupalGet('/admin/structure/importer/add');

$assert = $this->assertSession();

$assert->pageTextContains('Access denied');

We navigate to the form for creating importer configuration entities and assert that the user does not have access. This is because by default we are browsing as anonymous users. Next, we need to log in and try this again:

$this->drupalLogin($this->admin);

$this->drupalGet('/admin/structure/importer/add');

$assert->pageTextContains('Add importer');

$assert->elementExists('css', '#edit-label');

$assert->elementExists('css', '#edit-plugin');

$assert->elementExists('css', '#edit-update-existing');

$assert->elementExists('css', '#edit-source');

$assert->elementExists('css', '#edit-bundle');

$assert->elementNotExists('css', 'input[name="files[plugin_configuration_csv_file]"]');

We use the same drupalLogin() method and navigate back to the form. This time we assert that we have the title as well as various HTML elements—the form elements used for creating the entity. Moreover, we also assert that we do not have the element for uploading the CSV file because that should only show up if we select that we want to use the CSV Importer plugin.

It follows we do just that:

$page = $this->getSession()->getPage();

$page->selectFieldOption('plugin', 'csv');

$this->assertSession()->assertWaitOnAjaxRequest();

$assert->elementExists('css', 'input[name="files[plugin_configuration_csv_file]"]');  

Using the getSession() method, we get the current Mink session, from which we can get the object representing the actual page we are looking at. This is a DocumentElement object that can be traversed, inspected and manipulated in all sorts of ways. I recommend you check out the TraversableElement class for all the available methods.

One such method is selectFieldOption() by which we can specify the locator of an HTML select element (ID, name or label) and a value, and it will trigger the selection. As you know, this is supposed to make an Ajax request bringing in our new form elements. And using assertWaitOnAjaxRequest() on the JSWebAssert object, we can wait until that is complete. Finally, we can assert that the file upload field is present on the page.

Next, we proceed with filling in the form:

$page->fillField('label', 'Test CSV Importer');

$this->assertJsCondition('jQuery(".machine-name-value").html() == "test_csv_importer"');

$page->checkField('update_existing');

$page->fillField('source', 'testing');

$page->fillField('bundle', 'goods');

$wrapper = $this->container->get('stream_wrapper_manager')->getViaUri($this->file->getFileUri());

$page->attachFileToField('files[plugin_configuration_csv_file]', $wrapper->realpath());

$this->assertSession()->assertWaitOnAjaxRequest();

$page->pressButton('Save');

$assert->pageTextContains('Created the Test CSV Importer Importer.');

The generic fillField() method is useful for things such as text fields while the checkField() method is expectedly useful for checkboxes. The locator for both is again either the ID, the name, or the label of the element.

We also use the assertJsCondition method to have the execution wait until a JavaScript change has happened on the page. And we do this to ensure that the entity machine name field has been currently filled in.

Next, with the help of the stream wrapper of the file that we uploaded, and more specifically its realpath() method, we attach the file to the field using the attachFileToField() method. This triggers an Ajax request, which again we wait for to complete. Lastly, we use the pressButton() method to click on the submit button and then assert that we have a confirmation message printed out (the form has been saved and the page refreshed).

Now to check that the operation actually went through properly:

$config = Importer::load('test_csv_importer');

$this->assertInstanceOf(DrupalproductsEntityImporterInterface::class, $config);

$fids = $config->getPluginConfiguration()['file'];

$fid = reset($fids);

$file = File::load($fid);

$this->assertInstanceOf(DrupalfileFileInterface::class, $file);  

And the new use statements:

use DrupalfileEntityFile;

use DrupalproductsEntityImporter;  

We load the configuration entity using the ID we gave it and then assert that the resulting object is an instance of the correct interface. This checks whether we actually did save the entity. Next, we load the File entity based on the ID found in the Importer configuration entity and assert that it itself also implements the correct interface. This proves that the file actually got saved and the configuration is correct.

Instead of checking the rest of the field values programmatically, in the same way, we opt for navigating to the edit form of the Importer entity and asserting that the values are pre-filled correctly:

$this->drupalGet('admin/structure/importer/test_csv_importer/edit');

$assert->pageTextContains('Edit Test CSV Importer');

$assert->fieldValueEquals('label', 'Test CSV Importer');

$assert->fieldValueEquals('plugin', 'csv');

$assert->checkboxChecked('update_existing');

$assert->fieldValueEquals('source', 'testing');

$page->hasLink('products.csv');

$assert->fieldValueEquals('bundle', 'Goods (goods)');  

The fieldValueEquals() and checkboxChecked() methods are handy for checking field values. Moreover, we also use the hasLink() method to check whether there is a link with that name on the page. This is actually to prove the uploaded file is shown correctly:

Figure 17.1: Plugin configuration for CSV Importer

Figure 17.1: Plugin configuration for CSV Importer

And finally, since the bundle field is a reference field and not a simple text field, we need to construct the value the testing framework actually sees there, which is in this pattern: Label (ID).

And with this, our test is complete and we can run it in its entirety:

../vendor/bin/phpunit ../modules/custom/products/tests/src/Kernel/CsvImporterTest.php

Summary

In this chapter, we talked a bit about automated testing in Drupal 9. We started with an introduction about why it's useful and actually important to write automated tests, and then briefly covered a few of the more popular types of software development testing methodologies.

Drupal has the capability for quite a lot of methodologies, as we've seen. We have unit tests—the lowest level form of testing that focuses on single architectural units and which are by far the fastest running tests of them all. Then we have Kernel tests, which are integration tests focusing on lower-level components and their interactions. Next, we have Functional tests, which are higher-level tests that focus on interactions with the browser. And finally, we have the FunctionalJavascript tests, which extend the latter and bring Selenium and Chrome into the picture to allow the testing of functionalities that depend on JavaScript.

We've also seen that all these different types of tests are integrated with PHPUnit so we can run them all using this tool. This means that all the different types of tests follow the same "rules" for registering them with Drupal, namely, the directory placement, the namespacing, and the PHPDoc information.

The world of automated testing is huge and there can be no single chapter in a book that can cover all the different ways something can be tested. For this reason, especially for beginners, the journey toward good test coverage is full of trial and error and even has the occasional frustration. But out of this, we get stable code that works always and that is protected from regressions.

In the next and final chapter, we will take a look at a few things we can do to protect our Drupal applications from malicious attacks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.227.69