Install a testing suite
Links: Code in GitLab
Journey map:
- Part 1 — Configuring local development using Docker Compose
- Part 2 — Install a testing suite ← You are here
- Part 3 — Write automated tests
Note for the reader:
I wanted to write an article about how to write automated tests for a prestashop module. However in this process I found complicated just to install the testing suite, so I split up the two articles to avoid writing a too long article.
Choosing the tools
For automated testing I have chosen Codeception.
There are other alternatives, like PHPUnit, Behat or PHPSpec…
But in the end, I wanted to keep the things simple and Codeception let you create tests of all types, from unit to acceptance tests. This is the main reason for my choice.
For installing Codeception, we need to use composer
, which is the official PHP dependency manager.
If you don’t know what it is, this article should answer all your questions:
Installing Composer
Normally, to install composer, we need to first download it as a php archive (aka .phar ), using a command like this:
curl -sS https://getcomposer.org/installer | php
But this will install composer in our local project for our system PHP version. If you read the first part of this journey, you already know that our development configuration is all stored inside docker containers. So we don’t need to have PHP 5.6 installed on our machine, and everyone will use the exactly same php version. (someone yelled multiple projects?)
So what are the other choices?
The first one is using a docker image. Composer have an official image in docker hub. But there is a problem: It uses PHP 7 and there is not a PHP 5.6 tag. (Actually, the deprecated composer/composer
image have it, but it uses an old composer version and it will not be update in the future, so I don’t think it is a mantainable choice.)
The second solution is installing composer directly from one of our prestashop websites. But this would mean we need to enter in the prestashop container using bash, move into the directory module, running the composer command and exit from the container every time we need to do something with composer. Too many commands for something that should be simple.
The third one is creating our custom composer docker image, but with PHP 5.6. It is much more complicated to setup, but after the first time we can just use it. And if another guy want to start develop with us, we can just build the image again in his system and run it.
I will explore the third solution, because I think it is the most scalable with multiple developers working on the same project.
Creating the docker image
We need an image that is exactly the same to the official one but with PHP 5.6, the first thing that came to my mind was to copy paste the dockerfile of the official image and change the FROM command, but after browsing for a while I discovered that the old deprecated composer/composer
image was about half smaller, compared with to our builded image. Since I have an iMac with SSD, I would like to save some space if possible.
However, we need to store this dockerfile somewhere. Since we would have more dockerfiles in the future, I choose to create a build
folder in the root of our project, under where we will create another subfolder called composer
that will store the dockerfile.
This is the final result, with a size of 71 MB:
build/composer/Dockerfile
FROM php:5.6-alpine
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /tmp
RUN apk add --no-cache --virtual git subversion openssh mercurial tini bash patch \
&& echo "memory_limit=-1" > "$PHP_INI_DIR/conf.d/memory-limit.ini" \
&& echo "date.timezone=${PHP_TIMEZONE:-UTC}" > "$PHP_INI_DIR/conf.d/date_timezone.ini" \
&& apk add --no-cache --virtual .build-deps zlib-dev \
&& docker-php-ext-install zip \
&& runDeps="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \
| tr ',' '\n' \
| sort -u \
| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
)" \
&& apk add --virtual .composer-phpext-rundeps $runDeps \
&& apk del .build-deps \
&& rm -rf /var/cache/apk/* \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/bin/composer
WORKDIR /app
CMD ["-"]
ENTRYPOINT ["composer"]
So now we can just build the image
docker build -t neokree/composer ./build/composer/
After the process, we can finally install composer using our docker:
docker run --rm -it neokree/composer init
And composer will start the config generator:
Installing CodeCeption
When the procedure ends, we can finally install the test automation suite:
docker run --rm -it neokree/composer require codeception/codeception --dev
Voilà, we now have installed locally the test suite, only for our development environment.
Following the quick start of the suite, we can now bootstrap it, creating all necessary test directories, using this command:
./vendor/bin/codecept bootstrap
But again, this will use our local PHP version. So we need to use the php:5.6-alpine
image to create a container on the fly and use it instead of our local one.
docker run --rm -it -v $PWD:/app -w /app php:5.6-cli-alpine ./vendor/bin/codecept bootstrap
Even if this command works, I think that you are a little upset about the lenght of these command (just as I am), there should be a way to avoid call docker run --rm -it
and a lot of other parameters to do something.
Make it easy
To make things easies, we need to wrap the complicated docker commands under a clean interface, like the original command interface we would have if we just installed everything locally. We need a gateway.
There are multiple ways to do this:
- Aliases
- Makefile
- Bash scripting
But we are PHP developers. PHP is a scripting language. Why we cannot use it for write a script that wraps these commands?
Sure, in that case our script will use the default PHP version of the system, but I think this is acceptable. Every linux distribution, or macOS version that I know have it already installed as default. So I choose this path.
First, we need a place where store our scripts. Taking a cue from linux, I create a folder called bin
in the root of our project. Inside it we will start to create our binaries:
The composer script
Inside the folder we will create a composer
(without extension) file to wrap composer commands. But we should define how we will use it first, so we know what and how to implement it.
Since is a bin
folder, I would like to be able to use it directly calling it from php, as well to call it like an application, as well as including in my PATH, like all other system applications. Some examples:
Call from PHP:php bin/composer install
Call as application:./bin/composer install
Add to our local PATH and use it:
export PATH=$PWD/bin:$PATH
composer install
Ok now we know what we want, let’s start thinking how to do it.
So the script will:
- Build the docker image if not already built
- Pass the composer arguments to our composer docker container
- Show the composer output
It is not a lot of work, we just need to work a bit with shell_exec
and/or system
functions to make it work. But since we need to reuse some commands in the next script, we will abstract some functions for the future into a lib/docker.php
file.
This is the end result:
bin/composer
#!/usr/bin/env php
<?php
include_once('lib/docker.php');
$imageName = 'neokree/composer';
$arguments = implode(' ', array_slice($argv, 1));
$projectPath = realpath(__DIR__.'/../');
if (dockerImageDoesNotExist($imageName)) {
print "Composer image not found.\n";
print "Start building it. This may take a while...\n";
$dockerfileFolderPath = $projectPath."/build/composer/";
buildDockerImage($imageName, $dockerfileFolderPath);
}
$returnVal = executeCommandInDockerContainer($arguments, $imageName);
exit($returnVal);
bin/composer/lib/docker.php
<?php
/**
* @param string $imageName
* @return bool
*/
function dockerImageDoesNotExist($imageName)
{
return trim(shell_exec("docker image inspect $imageName:latest 2> /dev/null")) === '[]';
}
/**
* @param string $imageName
* @param string $dockerfileFolderPath
*/
function buildDockerImage($imageName, $dockerfileFolderPath)
{
shell_exec("docker build -t $imageName $dockerfileFolderPath");
}
/**
* @param string $command
* @param string $imageName
* @return int
*/
function executeCommandInDockerContainer($command, $imageName)
{
$returnVal = 0;
system('docker run --rm -it -v $PWD:/app -w /app '.$imageName.' '.$command, $returnVal);
return $returnVal;
}
One last thing: Remember to add the execution bits to the composer
file, otherwise you will be able to use it only as argument to PHP.
As of the previous script, we want the codeception
to be exactly like if we installed locally, but undeline we will pass everything into a docker container.
Since this time we don’t have to build nothing, the script will be much simpler:
bin/codecept
#!/usr/bin/env php
<?php
include_once('lib/docker.php');
$imageName = 'php:5.6-cli-alpine';
$arguments = implode(' ', array_slice($argv, 1));
$command = 'vendor/bin/codecept '.$arguments;
$returnVal = executeCommandInDockerContainer($command, $imageName);
exit($returnVal);
If you noted, this command will use our local codecept
binary, installed through composer. So in a new installation we will need to remember to install it first.
This ends Part 2. Thanks for sticking around!
In Part 3, we will start to write automated tests
Stay tuned!