Docker + Rails + System tests with Headless Chrome
Tags: docker, rails, headlesschrome, testing
Published: 2017-11-12 12:50:17

---
title: Docker + Rails + System tests with Headless Chrome
published: true
description: Headless chrome system testing inside a container using Ruby on Rails
tags: docker, rails, headless chrome, testing
---
# Goal
Show a simple setup of headless system testing inside a container with Ruby on Rails.  

There are likely many ways to achieve this goal.  However, my aim was to hook into an eventual CI system where the Rails application could be validated by building the docker image and running the test suite.

I'll break this down into 3 steps:
* Create docker image with CentOS and ruby 2.4
* Create docker images for Rails and headless chrome testing
* Configure Rails for headless chrome system testing

# Create docker image with CentOS and ruby 2.4

To accomplish this part I created a [simple project](https://github.com/dstull/ruby_24) which provides a base image for the Rails application. I decided to create this base image because I wanted to stick with CentOS, which at the time of creating, I wasn't able to find on [Docker Hub](https://hub.docker.com/) for ruby 2.4.

## Dockerfile

```Dockerfile
FROM centos:centos7

# Install the appropriate software
RUN yum -y update && yum clean all
RUN yum -y install \
			epel-release \
  		which \
 			&& \
 		yum clean all \
 			&& \
 		:

# Install rvm, default ruby version and bundler.

RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
RUN /bin/bash -l -c "curl -L get.rvm.io | bash -s stable"
RUN /bin/bash -l -c "echo 'source /etc/profile.d/rvm.sh' >> /etc/profile"
RUN /bin/bash -l -c "rvm install 2.4.2"
RUN /bin/bash -l -c "rvm cleanup all"
RUN /bin/bash -l -c "echo 'gem: --no-ri --no-rdoc' > .gemrc"
RUN /bin/bash -l -c "gem install bundler --no-ri --no-rdoc"
```

I then created a repository on [Docker Hub](https://hub.docker.com/), and setup an automated build against the [Github repo](https://github.com/dstull/ruby_24).  This allows it to automatically rebuild the image when changes are pushed to Github.

# Create docker images for Rails and headless chrome testing

Now that the base image for ruby is built and available on [Docker Hub](https://hub.docker.com/r/hammer098/ruby_24/), I can use it as the base in my example [Rails application](https://github.com/dstull/docker-rails).

Here starts the tricky part:

I wanted to keep the headless chrome pieces outside of the Rails application image that would go to production.  In order to do that, there are a few ways this can be accomplished:

1. Setup a container side by side with my application, and integrate using prebuilt [Selenium containers](http://blog.tomazy.com/2017/05/testing-rails-app-in-docker-containers.html).  I initially tried this, with the help of some Docker pros, but the test suite speed was just too slow (non headless setup/impatient developer/didn't try too hard to diagnose).

2. Setup a container with chrome installed along with the Rails application.  I ended up choosing this route, while accepting that the application image and the testing application image were not exactly the same.  

## Rails Application image setup

In a [build script](https://github.com/dstull/docker-rails/blob/master/sdlc/build) I performed the steps that would create the needed Dockerfile at runtime, and execute the build of the image.  This allows the specific application build steps to be kept common to this image and the test image.  Note that the Dockerfile should not be checked into the repo, but is in this application for demonstration purposes.

### Build script(rails image function)

```bash
build_rails() {
  cat Dockerfile.prefix.in \
  sdlc/Dockerfile.in \
  > Dockerfile

  time docker-compose build \
  rails
}
```

As seen above, I am concatenating the Ruby base piece ([Dockerfile.prefix.in](https://github.com/dstull/docker-rails/blob/master/Dockerfile.prefix.in)) with the common Rails part ([sdlc/Dockerfile.in](https://github.com/dstull/docker-rails/blob/master/sdlc/Dockerfile.in)), and then into the final Dockerfile that acts as the input for the docker-compose build.  In a real world setup the Dockerfile 'in' file pieces maybe a bit larger, and have a postfix with variables defined to tag the image.

### [Final Dockerfile](https://github.com/dstull/docker-rails/blob/master/Dockerfile)

```Dockerfile
FROM hammer098/ruby_24
# common rails build steps
WORKDIR /web

# Install rvm, default ruby version and bundler.
COPY .ruby-version /web/.ruby-version
COPY Gemfile /web/Gemfile
COPY Gemfile.lock /web/Gemfile.lock

RUN /bin/bash -l -c "bundle install"

COPY . /web
```

## Chrome + Rails Application image setup

In the [build script](https://github.com/dstull/docker-rails/blob/master/sdlc/build) similar steps were performed to craft the Dockerfile for the test image at time of build.

```bash
build_rails_test() {
  cat sdlc/Dockerfile.rails-test.in \
  sdlc/Dockerfile.in \
  > sdlc/Dockerfile.rails-test

  time docker-compose build \
  rails-test
}
```

In this function similar steps are being performed as in the Rails application build.  The steps unique to the test image are included in [sdlc/Dockerfile.rails-test.in](https://github.com/dstull/docker-rails/blob/master/sdlc/Dockerfile.rails-test.in). The common Rails part are then added, and sdlc/Dockerfile.rails-test is created to be used by the rails-test image.  Do not add sdlc/Dockerfile.rails-test to the repo as it is created 'on the fly' by this process.

### Final Dockerfile.rails-test

```Dockerfile
FROM hammer098/ruby_24

RUN yum -y install \
			chromedriver \
			chromium \
			gnu-free-sans-fonts \
			xorg-x11-server-Xvfb \
			&& \
			yum clean all \
			&& \
			:
# common rails build steps
WORKDIR /web

# Install rvm, default ruby version and bundler.
COPY .ruby-version /web/.ruby-version
COPY Gemfile /web/Gemfile
COPY Gemfile.lock /web/Gemfile.lock

RUN /bin/bash -l -c "bundle install"

COPY . /web
```

# Configure Rails for headless chrome system testing

For this part, I needed to configure 2 pieces:

## Rails test configuration

In this part we need to setup the headless chrome driver and enable specific chrome options.

### [application_system_test_case.rb](https://github.com/dstull/docker-rails/blob/master/test/application_system_test_case.rb)

```ruby
require 'test_helper'
require 'selenium-webdriver'
require 'capybara'

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase


  Capybara.register_driver :headless_chrome do |app|

    # options explained https://peter.sh/experiments/chromium-command-line-switches/
    # no-sandbox
    #   because the user namespace is not enabled in the container by default
    # headless
    #   run w/o actually launching gui
    # disable-gpu
    #   Disables graphics processing unit(GPU) hardware acceleration
    # window-size
    #   sets default window size in case the smaller default size is not enough
    #   we do not want max either, so this is a good compromise

    capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
        chromeOptions: { args: %w[no-sandbox headless disable-gpu window-size=1400,1400] }
    )

    Capybara::Selenium::Driver.new(
        app,
        browser:              :chrome,
        desired_capabilities: capabilities
    )
  end
  driven_by :headless_chrome
end
```

The corresponding example tests that use this configuration can be found [here.](https://github.com/dstull/docker-rails/tree/master/test/system)  An explanation of why I chose to use Rails system tests over rspec can be found [here.](https://dev.to/dstull/my-journey-from-rspec-feature-tests-to-rails-system-tests)

## Xvfb install and Docker entrypoint setup for test execution

In the setup at work we needed [xvfb](https://www.x.org/archive/X11R7.6/doc/man/man1/Xvfb.1.xhtml) installed for the tests to function properly.  However, we were also using an older chrome version(v59).  On this example setup I wasn't able to prove out that xvfb was required.  This was either due to the newer version of chrome (v62), or a less complex testing setup.

Setup:

1. Install the xvfb-run rpm as seen above in the test docker image.
2. Modify the entrypoint directive in docker-compose.yml to include xvfb-run and rails test execution steps.

### [docker-compose.yml](https://github.com/dstull/docker-rails/blob/master/docker-compose.yml) rails-test-run definition

```yaml
  rails-test-run:
    image: rails-test
    entrypoint:
      - bash
      - --login
      - xvfb-run
      - rails
      - test:system
      - RAILS_ENV=test
```

In the above I am telling docker via the command `docker-compose run --rm rails-test-run` to interactively login (bash --login), run xvfb, execute rails system tests, then exit.  The RAILS_ENV variable is set to test so that Rails doesn't by default first look at the development db setup.

Here is an example output of a test run utilizing a [test script](https://github.com/dstull/docker-rails/blob/master/sdlc/test):

```shell
docker-rails$ sdlc/test
Starting rails-test, running and exiting

Run options: --seed 22514

# Running:

Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.2-p198), codename: Russell's Teapot
* Min threads: 0, max threads: 1
* Environment: test
* Listening on tcp://0.0.0.0:35683
Use Ctrl-C to stop
...

Finished in 8.587443s, 0.3493 runs/s, 0.4658 assertions/s.
3 runs, 4 assertions, 0 failures, 0 errors, 0 skips
```

#Conclusion

I found this setup to be the one that worked in our eventual goal of hooking into a CI system.

The setup, as shown here, is far from complete for a Rails application destined for production.  Here are some of the things that I believe are out of scope and were left out of this article and the [example application](https://github.com/dstull/docker-rails):

1. Nginx/Unicorn install and setup
2. This container is running as root, in our production setup, we run as a non root user.
3. Setting container to read-only and adding VOLUME directives in the Dockerfile for certain areas Rails needs for writing. Changing of permissions/ownership to those files and directories at build time.
4. Mysql containers for test/development.
5. Environment variable breakout.
6. Handling of compiled assets and js runtime env.
...many more