At the project configuration level, we take the case of a site configuration exported globally, as opposed to a configuration placed in modules.
There are many articles and documentation pages on PHPUnit with Drupal, this article will focus on the use aspect in a project, even if a small reminder (very simplified) will be made to be able to pose the problem and how to remedy it.
PHPUnit in Drupal
The PHPUnit testing framework has been added to Drupal 8 core since the early days of its development (see change records from 2013). Previously there was a Drupal-specific tool "Simpletest", the PHPUnit initiative converted the kernel's Simpletest tests into PHPUnit tests.
Currently there are 4 types of PHPUnit tests:
- Unit: To test the methods of a class individually.
- No database.
- Need to "mocker" everything that is going to be used by the code (services, result of method calls, etc.)
- Kernel: For testing interactions between classes.
- Connection to a database possible, but need to explicitly list the tables that will be installed.
- Modules can be activated, but need to be listed individually (no recursive dependency management).
- Functional : To test a complete site.
- Connection to a database.
- Site installed via a profile.
- Possibility of activating modules (and dependencies are managed).
- Functional Javascript: Same as Functional but with a browser emulator so you can test Javascript execution.
Problematic
The difficulty of implementation lies in the "setup", setting up the prerequisites for the test to be executed. We're going to stick here with tests where the site is self-sufficient, with no external dependencies such as Redis, SolR, another Drupal site (see the Entity Share module ;) ), etc.
For Unit tests, everything needs to be mocked up and when the code changes, even just a little, these tests will generally need to be adjusted.
For Kernel, Functional and Functional Javascript type tests, you need to activate the necessary modules, then you need to set up the necessary configuration, then create the content, etc. Before you can test.
Example: a test that checks whether a view displays the right results is simple, just go to the view URL and test whether the results displayed are the right ones. The work is in the configuration, content creation, etc.
In Drupal core and community modules, the setup of certain tests is often tedious, but fortunately there are cases where it's simple, as much once it's in place, in general, it won't move around too much and the tests ensure the stability of the module.
Only on a project where the configuration is exported globally and changes during the life of the project, the aim is to avoid in the test setup, at the very least, the configuration part.
By relying on the exported configuration to test its operation and not testing a configuration recreated in the tests for the tests, we avoid the problem of having to maintain the configuration in several places.
Hence the problem is that in automated tests, it is not possible to import the exported configuration onto a project because the test is isolated from it.
We'll look at how to remedy this.
Solution
Since it is possible in tests to activate modules and for the configuration of these modules to be imported, the idea is to automatically generate a module containing the site configuration before the tests are launched and to import the configuration via this module.
This ensures that the tests are based on the site configuration and avoids having to maintain the configuration in several places in the project.
This tip was presented to me by Benjamin Rambaud on his https://gitlab.com/beram-drupal/drupal-ci repository.
So I implemented this logic on my project template, and created a dedicated branch with configuration and example tests. I took the example tests a step further with Functional and Functional Javascript type tests, which led to difficulties with Gitlab CI.
Implementation and problems encountered
Here are the steps for including this test system:
- Reworking and updating the configuration module creation script:https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project/-/blob/8.x/scripts/tests/manage-tests-module.sh
- Adapting the PHPUnit configuration file: https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project/-/blob/8.x/scripts/tests/phpunit.xml.dist
- I tried the Sqlite in-memory database for testing, which worked. But I eventually went back to the same database I used in Docker Compose because I might as well be identical with the project database.
- Where before I followed the PHPUnit file in the Drupal core, now only the custom code is listed. Incidentally, in the case of modules with sub-modules that have their own tests, the sub-modules must be listed explicitly. Too bad the wildcard ** doesn't work.
- Adapting the script to run the PHPUnit command: https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project/-/blob/8.x/scripts/tests/run-tests-phpunit.sh
- In my scripts, I try to make paths relative or managed by variables. However, the phpunit command is unable to take into account a relative path to indicate the location of the PHPUnit configuration file. You have to use an absolute path. Fortunately, Gitlab CI provides the "CI_PROJECT_DIR" variable, so I slightly adapted the paths in the Shell scripts to take it into account if necessary at the beginning of the file https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project/-/blob/8.x/scripts/script-parameters.sh
With these steps it is possible to have Unit and Kernel type PHPUnit tests. This corresponds to this commit.
Even though on my development environment I had what I needed for Functional and Functional Javascript tests, I was going to have to adapt the Gitlab CI configuration for this to work. Indeed, these types of tests need a working web server and a browser emulator.
Hence this commit and the adjustments:
- The .ht.router.php file is for one use of PHP's embedded web server. I'd never used it before, but to keep things simple and avoid having to make and maintain an extra Docker image or modify an existing one, I admit it's handy. Hence no longer excluding it from the Drupal scaffold plugin.
- The next difficulty was at the Docker network level in Gitlab CI because the web server is in the PHP container with the sources, so the URL of the site is 127.0.0.1, which works for Functional tests but not Functional Javascript because the browser emulator is in another container (chromedriver) so for this container, at the address 127.0.0.1 there is no site. Looking at how Drupal Spoons worked, I saw that there was the use of a "build" alias, and it's only in a sentence not highlighted in the Gitlab CI documentation that it's explained that automatically the container used for the task is accessible via the "build" alias. And well done in the PHPUnit configuration, the environment variables in the file are overloadable, so it's simple to have the configuration file ready for the development environment and overloaded via environment variables in Gitlab CI.
- All that remained were permission issues, wrong paths (due to Drupal paranoia for some).
From now on, all 4 types of PHPUnit tests will be run on Gitlab CI.
Next steps
Since this is a recent addition to the project template, there will undoubtedly be improvements and adjustments made. That's why I'm putting links to files and commits and not code in this article.
One aspect that I don't yet manage with Gitlab CI is artefact generation, which means that test output html files are not accessible, which can be restrictive in the event of a debug if the tests are run locally and not on Gitlab CI for example.
In 2017, I had initialised a Behat test configuration, I'll have to re-test it if it's still valid and try to make it executable in Gitlab CI. Pa11y accessibility testing will follow.
Unlike PHPUnit, which creates an entire environment and destroys it once testing is complete, Behat and Pa11y are tools that run on an existing environment.
Acknowledgements
Thanks to Benjamin Rambaud for showing me this trick.
Thanks also to Fabien Clément for showing me a trick allowing tests to import a project's exported configuration. A trick that I preferred not to use as it was more complex in terms of reasoning I found.
Thanks to Smile for allowing me to spend some time on this topic.