367 lines
12 KiB
Markdown
367 lines
12 KiB
Markdown
|
---
|
|||
|
title: Creating a Custom PHPUnit Command for Docksal
|
|||
|
date: 2018-05-06
|
|||
|
excerpt:
|
|||
|
How to write custom commands for Docksal, including one to easily run PHPUnit
|
|||
|
tests in Drupal 8.
|
|||
|
tags:
|
|||
|
- docksal
|
|||
|
- drupal
|
|||
|
- drupal-8
|
|||
|
- drupal-planet
|
|||
|
- phpunit
|
|||
|
- testing
|
|||
|
---
|
|||
|
|
|||
|
This week I’ve started writing some custom commands for my Drupal projects that
|
|||
|
use Docksal, including one to easily run PHPUnit tests in Drupal 8. This is the
|
|||
|
process of how I created this command.
|
|||
|
|
|||
|
## What is Docksal?
|
|||
|
|
|||
|
Docksal is a local Docker-based development environment for Drupal projects and
|
|||
|
other frameworks and CMSes. It is our standard tool for local environments for
|
|||
|
projects at [Microserve][0].
|
|||
|
|
|||
|
There was a [great talk][1] recently at Drupaldelphia about Docksal.
|
|||
|
|
|||
|
## Why write a custom command?
|
|||
|
|
|||
|
One of the things that Docksal offers (and is covered in the talk) is the
|
|||
|
ability to add custom commands to the Docksal’s `fin` CLI, either globally or as
|
|||
|
part of your project.
|
|||
|
|
|||
|
As an advocate of automated testing and TDD practitioner, I write a lot of tests
|
|||
|
and run PHPUnit numerous times a day. I’ve also given [talks][6] and have
|
|||
|
[written other posts][7] on this site relating to testing in Drupal.
|
|||
|
|
|||
|
There are a couple of ways to run PHPUnit with Docksal. The first is to use
|
|||
|
`fin bash` to open a shell into the container, move into the docroot directory
|
|||
|
if needed, and run the `phpunit` command.
|
|||
|
|
|||
|
```bash
|
|||
|
fin bash
|
|||
|
cd /var/www/docroot
|
|||
|
../vendor/bin/phpunit -c core modules/custom
|
|||
|
```
|
|||
|
|
|||
|
Alternatively, it can be run from the host machine using `fin exec`.
|
|||
|
|
|||
|
```
|
|||
|
cd docroot
|
|||
|
fin exec '../vendor/bin/phpunit -c core modules/custom'
|
|||
|
```
|
|||
|
|
|||
|
Both of these options require multiple steps as we need to be in the `docroot`
|
|||
|
directory where the Drupal code is located before the command can be run, and
|
|||
|
both have quite long commands to run PHPUnit itself - some of which is repeated
|
|||
|
every time.
|
|||
|
|
|||
|
By adding a custom command, I intend to:
|
|||
|
|
|||
|
1. Make it easier to get set up to run PHPUnit tests - i.e. setting up a
|
|||
|
`phpunit.xml` file.
|
|||
|
1. Make it easier to run the tests that we’d written by shortening the command
|
|||
|
and making it so it can be run anywhere within our project.
|
|||
|
|
|||
|
I also hoped to make it project agnostic so that I could add it onto any project
|
|||
|
and immediately run it.
|
|||
|
|
|||
|
## Creating the command
|
|||
|
|
|||
|
Each command is a file located within the `.docksal/commands` directory. The
|
|||
|
filename is the name of the command (e.g. `phpunit`) with no file extension.
|
|||
|
|
|||
|
To create the file, run this from the same directory where your `.docksal`
|
|||
|
directory is:
|
|||
|
|
|||
|
```bash
|
|||
|
mkdir -p .docksal/commands
|
|||
|
touch .docksal/commands/phpunit
|
|||
|
```
|
|||
|
|
|||
|
This will create a new, empty `.docksal/commands/phpunit` file, and now the
|
|||
|
`phpunit` command is now listed under "Custom commands" when we run `fin`.
|
|||
|
|
|||
|
![](/images/blog/docksal-phpunit-command/1.gif)
|
|||
|
|
|||
|
You can write commands with any interpreter. I’m going to use bash, so I’ll add
|
|||
|
the shebang to the top of the file.
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
```
|
|||
|
|
|||
|
With this in place, I can now run `fin phpunit`, though there is no output
|
|||
|
displayed or actions performed as the rest of the file is empty.
|
|||
|
|
|||
|
## Adding a description and help text
|
|||
|
|
|||
|
Currently the description for our command when we run `fin` is the default "No
|
|||
|
description" text. I’d like to add something more relevant, so I’ll start by
|
|||
|
adding a new description.
|
|||
|
|
|||
|
fin interprets lines starting with `##` as documentation - the first of which it
|
|||
|
uses as the description.
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
|
|||
|
## Run automated PHPUnit tests.
|
|||
|
```
|
|||
|
|
|||
|
Now when I run it, I see the new description.
|
|||
|
|
|||
|
![](/images/blog/docksal-phpunit-command/2.gif)
|
|||
|
|
|||
|
Any additional lines are used as help text with running `fin help phpunit`. Here
|
|||
|
I’ll add an example command to demonstrate how to run it as well as some more
|
|||
|
in-depth text about what the command will do.
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
|
|||
|
## Run automated PHPUnit tests.
|
|||
|
##
|
|||
|
## Usage: fin phpunit <args>
|
|||
|
##
|
|||
|
## If a core/phpunit.xml file does not exist, copy one from elsewhere.
|
|||
|
## Then run the tests.
|
|||
|
```
|
|||
|
|
|||
|
Now when I run `fin help phpunit`, I see the new help text.
|
|||
|
|
|||
|
![](/images/blog/docksal-phpunit-command/3.gif)
|
|||
|
|
|||
|
## Adding some content
|
|||
|
|
|||
|
### Setting the target
|
|||
|
|
|||
|
As I want the commands to be run within Docksal’s "cli" container, I can specify
|
|||
|
that with `exec_target`. If one isn’t specified, the commands are run locally on
|
|||
|
the host machine.
|
|||
|
|
|||
|
```
|
|||
|
#: exec_target = cli
|
|||
|
```
|
|||
|
|
|||
|
### Available variables
|
|||
|
|
|||
|
These variables are provided by fin and are available to use within any custom
|
|||
|
commands:
|
|||
|
|
|||
|
- `PROJECT_ROOT` - The absolute path to the nearest `.docksal` directory.
|
|||
|
- `DOCROOT` - name of the docroot folder.
|
|||
|
- `VIRTUAL_HOST` - the virtual host name for the project. Such as
|
|||
|
`myproject.docksal`.
|
|||
|
- `DOCKER_RUNNING` - (string) "true" or "false".
|
|||
|
|
|||
|
<div class="note" markdown="1">
|
|||
|
**Note:** If the `DOCROOT` variable is not defined within the cli container, ensure that it’s added to the environment variables in `.docksal/docksal.yml`. For example:
|
|||
|
|
|||
|
```
|
|||
|
version: "2.1"
|
|||
|
|
|||
|
services:
|
|||
|
cli:
|
|||
|
environment:
|
|||
|
- DOCROOT
|
|||
|
```
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
### Running phpunit
|
|||
|
|
|||
|
When you run the `phpunit` command, there are number of options you can pass to
|
|||
|
it such as `--filter`, `--testsuite` and `--group`, as well as the path to the
|
|||
|
tests to execute, such as `modules/custom`.
|
|||
|
|
|||
|
I wanted to still be able to do this by running `fin phpunit <args>` so the
|
|||
|
commands can be customised when executed. However, as the first half of the
|
|||
|
command (`../vendor/bin/phpunit -c core`) is consistent, I can wrap that within
|
|||
|
my custom command and not need to type it every time.
|
|||
|
|
|||
|
By using `"$@"` I can capture any additional arguments, such as the test
|
|||
|
directory path, and append them to the command to execute.
|
|||
|
|
|||
|
I’m using `$PROJECT_ROOT` to prefix the command with the absolute path to
|
|||
|
`phpunit` so that I don’t need to be in that directory when I run the custom
|
|||
|
command, and `$DOCROOT` to always enter the sub-directory where Drupal is
|
|||
|
located. In this case, it’s "docroot" though I also use "web" and I’ve seen
|
|||
|
various others used.
|
|||
|
|
|||
|
```bash
|
|||
|
DOCROOT_PATH="${PROJECT_ROOT}/${DOCROOT}"
|
|||
|
DRUPAL_CORE_PATH="${DOCROOT_PATH}/core"
|
|||
|
|
|||
|
# If there is no phpunit.xml file, copy one from elsewhere.
|
|||
|
|
|||
|
# Otherwise run the tests.
|
|||
|
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
|
|||
|
```
|
|||
|
|
|||
|
For example, `fin phpunit modules/custom` would execute
|
|||
|
`/var/www/vendor/bin/phpunit -c /var/www/docroot/core modules/custom` within the
|
|||
|
container.
|
|||
|
|
|||
|
I can then wrap this within a condition so that the tests are only run when a
|
|||
|
`phpunit.xml` file exists, as it is required for them to run successfully.
|
|||
|
|
|||
|
```bash
|
|||
|
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
|
|||
|
# If there is no phpunit.xml file, copy one from elsewhere.
|
|||
|
else
|
|||
|
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
|
|||
|
fi
|
|||
|
```
|
|||
|
|
|||
|
### Creating phpunit.xml - step 1
|
|||
|
|
|||
|
My first thought was that if a `phpunit.xml` file doesn’t exist was to duplicate
|
|||
|
core’s `phpunit.xml.dist` file. However this isn’t enough to run the tests, as
|
|||
|
values such as `SIMPLETEST_BASE_URL`, `SIMPLETEST_DB` and
|
|||
|
`BROWSERTEST_OUTPUT_DIRECTORY` need to be populated.
|
|||
|
|
|||
|
As the tests wouldn't run at this point, I’ve exited early and displayed a
|
|||
|
message to the user to edit the new `phpunit.xml` file and run `fin phpunit`
|
|||
|
again.
|
|||
|
|
|||
|
```bash
|
|||
|
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
|
|||
|
echo "Copying ${DRUPAL_CORE_PATH}/phpunit.xml.dist to ${DRUPAL_CORE_PATH}/phpunit.xml."
|
|||
|
echo "Please edit it's values as needed and re-run 'fin phpunit'."
|
|||
|
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
|
|||
|
exit 1;
|
|||
|
else
|
|||
|
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
|
|||
|
fi
|
|||
|
```
|
|||
|
|
|||
|
However this isn’t as streamlined as I originally wanted as it still requires
|
|||
|
the user to perform an additional step before the tests can run.
|
|||
|
|
|||
|
### Creating phpunit.xml - step 2
|
|||
|
|
|||
|
My second idea was to keep a pre-configured file within the project repository,
|
|||
|
and to copy that into the expected location. That approach would mean that the
|
|||
|
project specific values would already be populated, as well as any
|
|||
|
customisations made to the default settings. I decided on
|
|||
|
`.docksal/drupal/core/phpunit.xml` to be the potential location.
|
|||
|
|
|||
|
Also, if this file is copied then we can go ahead and run the tests straight
|
|||
|
away rather than needing to exit early.
|
|||
|
|
|||
|
If a pre-configured file doesn’t exist, then we can default back to copying
|
|||
|
`phpunit.xml.dist`.
|
|||
|
|
|||
|
To avoid duplication, I created a reusable `run_tests()` function so it could be
|
|||
|
executed in either scenario.
|
|||
|
|
|||
|
```bash
|
|||
|
run_tests() {
|
|||
|
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
|
|||
|
}
|
|||
|
|
|||
|
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
|
|||
|
if [ -e "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ]; then
|
|||
|
echo "Copying ${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml to ${DRUPAL_CORE_PATH}/phpunit.xml"
|
|||
|
cp "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ${DRUPAL_CORE_PATH}/phpunit.xml
|
|||
|
run_tests "$@"
|
|||
|
else
|
|||
|
echo "Copying ${DRUPAL_CORE_PATH}/phpunit.xml.dist to ${DRUPAL_CORE_PATH}/phpunit.xml."
|
|||
|
echo "Please edit it's values as needed and re-run 'fin phpunit'."
|
|||
|
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
|
|||
|
exit 1;
|
|||
|
fi
|
|||
|
else
|
|||
|
run_tests "$@"
|
|||
|
fi
|
|||
|
```
|
|||
|
|
|||
|
This means that I can execute less steps and run a much shorter command compared
|
|||
|
to the original, and even if someone didn’t have a `phpunit.xml` file created
|
|||
|
they could have copied into place and have tests running with only one command.
|
|||
|
|
|||
|
## The finished file
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
|
|||
|
#: exec_target = cli
|
|||
|
|
|||
|
## Run automated PHPUnit tests.
|
|||
|
##
|
|||
|
## Usage: fin phpunit <args>
|
|||
|
##
|
|||
|
## If a core/phpunit.xml file does not exist, one is copied from
|
|||
|
## .docksal/core/phpunit.xml if that file exists, or copied from the default
|
|||
|
## core/phpunit.xml.dist file.
|
|||
|
|
|||
|
DOCROOT_PATH="${PROJECT_ROOT}/${DOCROOT}"
|
|||
|
DRUPAL_CORE_PATH="${DOCROOT_PATH}/core"
|
|||
|
|
|||
|
run_tests() {
|
|||
|
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
|
|||
|
}
|
|||
|
|
|||
|
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
|
|||
|
if [ -e "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ]; then
|
|||
|
echo "Copying ${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml to ${DRUPAL_CORE_PATH}/phpunit.xml"
|
|||
|
cp "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ${DRUPAL_CORE_PATH}/phpunit.xml
|
|||
|
run_tests "$@"
|
|||
|
else
|
|||
|
echo "Copying phpunit.xml.dist to phpunit.xml"
|
|||
|
echo "Please edit it's values as needed and re-run 'fin phpunit'."
|
|||
|
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
|
|||
|
exit 0;
|
|||
|
fi
|
|||
|
else
|
|||
|
run_tests "$@"
|
|||
|
fi
|
|||
|
```
|
|||
|
|
|||
|
It’s currently available as a [GitHub Gist][2], though I’m planning on moving it
|
|||
|
into a public GitHub repository either on my personal account or the [Microserve
|
|||
|
organisation][3], for people to either use as examples or to download and use
|
|||
|
directly.
|
|||
|
|
|||
|
I’ve also started to add other commands to projects such as `config-export` to
|
|||
|
standardise the way to export configuration from Drupal 8, run Drupal 7 tests
|
|||
|
with SimpleTest, and compile front-end assets like CSS within custom themes.
|
|||
|
|
|||
|
I think it’s a great way to shorten existing commands, or to group multiple
|
|||
|
commands into one like in this case, and I can see a lot of other potential uses
|
|||
|
for it during local development and continuous integration. Also being able to
|
|||
|
run one command like `fin init` and have it set up everything for your project
|
|||
|
is very convenient and a big time saver!
|
|||
|
|
|||
|
<div class="note" markdown="1">
|
|||
|
Since writing this post, I’ve had a [pull request][8] accepted for this command to be added as a [Docksal add-on][9]. This means that the command can be added to any Docksal project by running `fin addon install phpunit`. It will be installed into the `.docksal/addons/phpunit` directory, and displayed under "Addons" rather than "Custom commands" when you run `fin`.
|
|||
|
</div>
|
|||
|
|
|||
|
## Resources
|
|||
|
|
|||
|
- [PHPUnit](https://phpunit.de)
|
|||
|
- [PHPUnit in Drupal 8][4]
|
|||
|
- [Main Docksal website](https://docksal.io)
|
|||
|
- [Docksal documentation](https://docksal.readthedocs.io)
|
|||
|
- [Docksal: one tool to rule local and CI/CD environments][1] - Docksal talk
|
|||
|
from Drupaldelphia
|
|||
|
- [phpcs example custom command][5]
|
|||
|
- [phpunit command Gist][2]
|
|||
|
- [Docksal addons blog post][9]
|
|||
|
- [Docksal addons repository][10]
|
|||
|
|
|||
|
[0]: {{site.companies.microserve.url}}
|
|||
|
[1]: https://youtu.be/1sjsvnx1P7g
|
|||
|
[2]: https://gist.github.com/opdavies/72611f198ffd2da13f363ea65264b2a5
|
|||
|
[3]: {{site.companies.microserve.github}}
|
|||
|
[4]: https://www.drupal.org/docs/8/phpunit
|
|||
|
[5]:
|
|||
|
https://github.com/docksal/docksal/blob/develop/examples/.docksal/commands/phpcs
|
|||
|
[6]: /talks/tdd-test-driven-drupal
|
|||
|
[7]: /articles/tags/testing
|
|||
|
[8]: https://github.com/docksal/addons/pull/15
|
|||
|
[9]: https://blog.docksal.io/installing-addons-in-a-docksal-project-172a6c2d8a5b
|
|||
|
[10]: https://github.com/docksal/addons
|