Running unit tests for front-end web applications require them to be tested in a web browser. While it's not an issue on a workstation, it can become tedious when running in a restricted environment such as a Docker container. In fact, these execution environments are generally lightweight and do not contain any graphical environment.
One solution to work around this issue is to use a headless web browser designed for development purposes, like PhantomJS. While it's an elegant solution for testing an application, it would be even better to test it directly in a web browser which will be used by the end-users in order to match real conditions of use, for examples Firefox or Chromium/Google Chrome. However, as mentioned above, it is needed to find a way to execute a regular web browser in a restricted environment.
The main issue is that regular web browsers need a graphical environment to be executed. A widely used workaround is to rely on xvfb to provide "an X server that can run on machines with no display hardware and no physical input devices". While this method works well, it requires you to install additional software on the restricted environment and the configuration can be a bit tricky. Thankfully for us, some web browsers designed for end-users now come with a headless mode. This last one doesn't require any graphical rendering server to work. Chromium/Google Chrome has already implemented this feature while Mozilla is still working on it to add it to Firefox.
So, what are we waiting for? Let's create a testing environment in a Docker container! For that purpose, the testing environment will rely on the official Node.js Docker image as base image and Chromium as web browser. I will suppose that you have an Angular project bootstrapped with Angular CLI.
Chromium/Google Chrome is shipped with the headless mode since version 59. The
official Node.js Docker image is based on Debian Jessie by default and to this
date, the latest version of Chromium in Debian Jessie's repositories is 57 since
it is 59 for Debian Stretch. It is possible to use an official Node.js Docker
image based on Debian Stretch using the appropriate tag. In our case,
8-stretch
:
FROM docker.io/node:8-stretch
LABEL net.skyplabs.maintainer-name="Paul-Emmanuel Raoul"
LABEL net.skyplabs.maintainer-email="skyper@skyplabs.net"
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends chromium
ENV CHROME_BIN=chromium
WORKDIR /usr/src/app
CMD ["npm", "start"]
Now, it is needed to configure Karma to use Chromium with the headless mode. The
karma-chrome-launcher supports natively
ChromeHeadless
as web browser. To define it as default web browser in
karma.conf.js
:
...
browsers: ['ChromeHeadless'],
...
It is now possible to build the Docker image and to use it to run the unit
tests. Navigate to the folder containing the source code of your Angular project
and the above Dockerfile
, then:
docker build -t angular-dev .
docker run --rm -v $(pwd):/usr/src/app:z angular-dev npm test
Everything should work well. However, on some continuous integration
environments, the Chromium's sandbox feature can present a
problem. To fix it, add an additional web browser
called ChromeHeadlessCI
to the Karma configuration which will be based on
ChromeHeadless
but with the --no-sandbox
flag this time:
...
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
...
Also, increase the browser's no activity timeout to be sure that the continuous integration pipeline doesn't fail because the duration value is too short:
...
browserNoActivityTimeout: 60000
...
The complete karma.conf.js
should look like this:
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
files: [
{ pattern: './src/test.ts', watched: false }
],
preprocessors: {
'./src/test.ts': ['@angular/cli']
},
mime: {
'text/x-typescript': ['ts','tsx']
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: config.angularCli && config.angularCli.codeCoverage
? ['progress', 'coverage-istanbul']
: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
singleRun: false,
browserNoActivityTimeout: 60000
});
};
Finally, add an npm script to run ng test
using ChromeHeadlessCI
and with
the --single-run=true
option:
{
// ...
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build -e prod",
"test": "ng test",
"test:ci": "ng test --browser ChromeHeadlessCI --code-coverage=true --single-run=true",
"lint": "ng lint",
"e2e": "ng e2e"
},
// ...
}
With this done, it is now possible to run the continuous integration tests inside a Docker container:
docker run --rm -v $(pwd):/usr/src/app:z angular-dev npm run test:ci