Introduction

Unit testing brings many benefits to our projects by assuring that code mistakes could be captured early and not in the production. You don't need to remember every single logic you or other developers have used in the code before refactoring. You start refactoring and tests will tell you when something has broken.

There are many more benefits in unit testing and I don't want to repeat them but if you have not been convinced that they'll make your life easier, take a look at this article .

However, In this article, I'm not going to show you how to write unit tests or why we need them but how to run your Angular unit tests in Gitlab Continuous Integration (CI) environment or any Docker-based CI service.

Prerequisite

I've chosen Gitlab CI/CD since it supports Docker. We can easily replicate our build environment locally without having an active Gitlab account. Same configuration work for any other CI environment which supports Docker.

1. Docker

In this tutorial, I'm going to use Docker for creating an environment similar to GitLab and building a base image to use in our Gitlab runner. Hence you need to have Docker installed on your machine and be familiar with Docker. You need to have a Docker Hub or any other Docker registry account so you can push your image for using in Gitlab.

Install Docker from here .

2. Angular Application

For this tutorial, we need an Angular project with Karma unit tests. If you already have such a project you can use it otherwise we'll create a "Hello World" application for this tutorial.

3. Nodejs

Angular projects need Nodejs for building and running the tests. Install current active LTS version of node.js on your machine from here .

It'll automatically install npm package manager . You can verify the installation by running the following commands:

node --version 
npm --version

4. Angular CLI

Angular CLI is a command-line tool that we'll use to scaffold our Angular application. It'll create the project with the app component and a basic unit test for that component. Since our goal in this tutorial is not the unit test itself but running the tests on a CI environment, we'll use the existing tests in the project.

Install the Angular CLI globally using the following command:

npm install -g @angular/cli

And verify your installation by running the following command:

ng --version

5. Create "Hello World" Application

Run the following command to scaffold a new Angular project if you don't have any existing application:

ng new test-project

Verify your project has been successfully created by running the following command and navigate to your application in the browser:

cd test-project && npm start

If you can open your application in the browser, you have successfully set up your project. We'll explore the testing framework on this application in the next section.

Angular Tests

When we create an application using the Angular CLI, the default test suite is comprised of Jasmine and Karma.

Jasmine is an open-source testing framework which Angular uses by default. Its syntax is easy to read and comes with built-in test doubles and assertion library.

Karma is a test runner created by AngularJS team. It spawns a web server that executes tests in a browser and displays the results in the command line. Since Karma uses a browser for running the tests, the environment you want to run the tests in should have a browser.

This is different from React default testing framework (Jest) which uses a custom DOM ( jsdom ). Jsdom does not render HTML elements like a real browser and it only simulates browsers behaviour, hence it doesn't need a browser to be installed in the testing environment.

In karma.conf.js in your source code, you can see the configuration for your application test runner. You can see that there is an entry called browsers which takes an array of browser names to run your tests against. By default, it's been set to Chrome which means whenever when you run your tests it'll open up your Chrome browser and runs the tests in it.

Run the following command and make sure your tests are passing:

  npm run test
running tests locally

Run Tests in Docker

In the previous section, we saw that running the tests in our development environment is straight forward and works out of the box as expected. But the situation is different in command-line environments like Docker or most of the continuous integration tools.

We are going to use Docker since many CI services including Gitlab use Docker for their build process. If we can run our tests successfully in a Docker container locally, it'll guarantee that it will run without any problem on Gitlab or any other build tool that supports Docker.

Please note that creating a Docker image from your application is not required if you want to run your tests in Gitlab. We are doing this as a part of this tutorial to demonstrate how to run tests in a simulated CI environment.

In order to replicate an environment similar to the CI environment, we need to create a Docker image from our source code. Create a Dockerfile in the root of your application and use the following instructions to create your application Docker image:

  FROM node:12.7-alpine 
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENTRYPOINT npm run test

Run the following command to create your Angular application Docker image:

  docker build -t angular-test .

This Docker image uses node as the base image and copies your source code into the image. The entry point for the image is set to run the tests which mean whenever you run this container it runs your unit tests. Run the following command to create a container from your application image:

  docker run --rm angular-test

As we expected you can see that it throws an error complaining about a browser for running tests.

Error running tests inside docker container

For fixing this issue we need to have an image which has NodeJS and Chrome pre-installed which are the dependencies for running Karma tests. Since the Docker environment doesn't have any Graphical User Interface (GUI) we'll use Headless Chrome which is a full Chrome browser but without UI that is great for automated testing.

Please note that you can install your environment dependencies inside your pipeline but that approach is not efficient since you need to install dependencies every time your pipeline runs. Packaging your required tools (i.e. Chrome) into a custom Docker image makes running pipeline faster and you'll have a cleaner pipeline since your build tasks are not cluttered with the environment preparation scripts.

Create a Dockerfile using the following instructions:

  FROM node:13.10.1 

RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
RUN apt-get update && apt-get install -y google-chrome-stable

Here we are adding Google Chrome public key to apt to verify the signature of the package we want to download. Next, we add the Chrome source URL to the sources list and install the browser inside our Docker image. Now we need to build and push our image to a public registry so it can be used in our CI agent. I used the following command to build and push the image to my Docker Hub:

  docker build -t node-chrome:v1 . 
docker tag node-chrome:v1 svaseghi/node-chrome:v1
docker push svaseghi/node-chrome:v1

We need to tell Karma to use a different browser for our CI environment. Add the following entry to karma.conf.js:

Adding headless chrome profile to karma config

We define a new launcher which uses Chrome Headless browser. --no-sandbox and --disable-setuid-sandbox flags are required to be able to run Chrome Headless inside Docker. Although we've defined a new browser for launching our Karma tests, still Karma browsers is set to Chrome which is the real Chrome browser. We don't want to change the normal testing behaviour so we'll add another script to the package.json to use this new browser only in CI environments.

Adding a new script to run tests using headless browser

We also need to tell Karma where it can find the Chrome binary file. We do that by putting the chrome binary location in CHROME_BIN environment variable in the Dockerfile.

Now we can fix our Docker image using the following changes:
1. Use the new base image we created earlier which includes Chrome and NodeJS.
2. Set the Chrome binary location in the environment variable.
3. Use CI version of the tests which uses Headless Chrome instead of the real browser.

Modify your Dockerfile to apply these changes:

  FROM svaseghi/node-chrome:latest 
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV CHROME_BIN=/usr/bin/google-chrome
ENTRYPOINT npm run test:ci

And rebuild your Docker image using the following command:

  docker build -t angular-test .

Run your container using the following command and verify that your tests are running successfully inside Docker:

  docker run --rm angular-test
Running tests successfully in Docker

Now that we can run our tests in Docker, we can easily use the same configuration to run our tests in any Docker-based environment including Gitlab CI. In the next section, we'll configure our Gitlab CI file to run the tests.

Run Tests in Gitlab

Continuous Integration (CI) tools take the burden of many repetitive tasks off the developer's shoulder. One of the key benefits you gain from CI tools is the ability to automate the testing. With a good integration policy, they'll guarantee that merged code is always passing the tests and if your project has a good test coverage then you don't need to worry about regression. However, sometimes you need to take some extra steps to automate this process.

Many CI services including Gitlab, CircleCI and TeamCity support Docker which help you easily manage your pipeline dependencies. It means that with the Docker image we created in the last section you will be able to use any tools that supports Docker. However, the pipeline instruction and syntax for each service could be different.

To use our custom image for Gitlab CI modify or create .gitlab-ci.yml file in the root of your project:

  build: 
  stage: build
  image: svaseghi/node-chrome:v1
  before_script:
    - export CHROME_BIN=/usr/bin/google-chrome
    - npm ci

  script:
    - npm run build
    - npm run test:ci

We've used our custom image as the base image for the runner and defined Chrome binary location using CHROME_BIN environment variable. We are using the CI version of the tests which uses headless Chrome for running the tests. You can see that the build steps are clean and free from environment preparation scripts. Also, your custom image gets cached by Gitlab and the entire build process will be faster compared to when you install dependencies inside the build pipeline.

If you publish these changes, your tests should run successfully in the Gitlab CI/CD pipeline.