Run Rails 6 System Tests in Docker Using a Host Browser
Tags: rails, ruby, testing, docker
Published: 2020-05-13 18:16:41

---
title: Run Rails 6 System Tests in Docker Using a Host Browser
published: true
date: 2020-05-11 18:40:10 UTC
tags: rails,ruby,testing,docker
canonical_url: https://avdi.codes/run-rails-6-system-tests-in-docker-using-a-host-browser/
cover_image: https://i2.wp.com/avdi.codes/wp-content/uploads/2020/05/dreamstime_m_31056486.jpg?zoom=2&resize=1000%2C750&ssl=1
---

The problem: we wanted to get Rails system tests running using a browser on our development machines.

The complication: we develop our Rails project within Docker, in order to make every aspect of our development environment reproduceable.

The preference: we want to **see** the browser-based tests running, but we don’t want to mess around with running browser _inside_ a container and tunneling its display to an external X-Windows server. We already have perfectly good browsers on our host machines, and we’d like to use them natively.

The outcome: we can run Rails system tests inside a Docker container. It pops up Chrome windows on the host machine, and exercises our app!

Coming up with the steps below involved a lot of trial-and-error, research, and debugging. I’m going to skip all that for this write-up, and just break down our working configuration piece by piece.

I’ve also written up an [in-depth breakdown of what all these parts are, and how they work together in Rails system tests](https://avdi.codes/rails-6-system-tests-from-top-to-bottom/).

## 1. Disable the webdrivers gem

The webdrivers gem is a lovely convenience for making sure you have the right browser and WebDriver tool installed before running system tests. However, we don’t want to run the browser inside the container. We want the container to talk to a browser either on the Docker host machine, or in a dedicated selenium sidecar container.

The `webdrivers` gem was causing system tests to fail with “couldn’t find browser” error. So we disabled it.

```
# Gemfile
​
group :test do
  # ...
  # webdrivers
end
```

And then ran `bundle install` again to update `Gemfile.lock`.

## 2. Choose a stable port for the test server

By default Capybara picks a new random port to run the test web server on every time system tests are run. We want tools _outside_ the container to be able to consistently communicate with the test server.

We arbitrarily chose port 3434, and documented that choice with an entry in a brand-new `.env` file in the project root.

```
CAPYBARA\_SERVER\_PORT=3434
```

## 3. Configure Docker-Compose for external browser testing

Here’s an elided version of our `.devcontainer/docker-compose.yml`, with the important elements commented.

```
# Specify a recent version of docker-compose to ensure we have fancy environment 
# variable expansion
version: '3.2'
services:
  app:
    build: 
      context: .
      dockerfile: Dockerfile
​
    environment:
      # Convey which port Capybara should be starting test servers on; fail loudly
      # if we've neglected to configure it.
      CAPYBARA_SERVER_PORT: "${CAPYBARA_SERVER_PORT:-3434}"
      # The test server WILL NOT BE AVAILABLE from outside the container if it binds 
      # to 127.0.0.1. It must bind to 0.0.0.0 to be exposed to the outside world.
      CAPYBARA_SERVER_HOST: "${CAPYBARA_SERVER_HOST:-0.0.0.0}"
    
    volumes:
      - type: bind
        source: ..
        target: /workspace
        consistency: cached
​
    ports:
      # Expose the test server port. Fail loudly if CAPYBARA_SERVER_PORT isn't set.
      - "${CAPYBARA_SERVER_PORT:-3434}:${CAPYBARA_SERVER_PORT:-3434}"
      # Also expose the usual app port
      - "3000:3000"
```

## 4. Configure Capybara to use the test server and port from environment

We added two lines to our `test/test_helper.rb`:

```
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
​
# Use `fetch` to fail loudly if these variables aren't set. We might relax this 
# and set defaults at some point, but for the moment we want to make sure we didn't 
# miss a step.
Capybara.server_host = ENV.fetch("CAPYBARA_SERVER_HOST")
Capybara.server_port = ENV.fetch("CAPYBARA_SERVER_PORT")
​
class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)
​
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all
​
  # Add more helper methods to be used by all tests here...
end
```

## 5. Update the system test base class to drive a remote browser

The configuration so far makes sure that Capybara starts up the server-under-test in a way that external tools can find it and talk to it. But we still needed to tell Capybara to interact with use an external WebDriver, instead of trying to drive a browser inside the container.

We updated our `test/application_system_test.rb`:

```
require "test_helper"
​
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  parallelize(workers: 1)
  driven_by :selenium, using: :chrome, screen_size: [1400, 1400],
                       options: {
                         browser: :remote,
                         url: "http://host.docker.internal:9515",
                       }
end
```

Notes:

- Figuring out the right options to pass into `driven_by` was… nontrivial. 
- `browser: :remote` are the magic options to tell Capybara _not_ to try to talk to a local WebDriver tool.
- `host.docker.internal` is a special hostname that Docker sets up to refer to the host machine.
- Port `9515` is the standard port that `chromedriver` listens for commands on. In a future revision I think I’d like to capture this knowledge more explicitly in another environment variable.

## 6. Install ChromeDriver on the development host

Our first development host happened to be a Windows machine so we used:

```
choco install -y chromedriver
```

Instructions for installing ChromeDriver on various platforms can be found at [the ChromeDriver project home page](https://sites.google.com/a/chromium.org/chromedriver/).

## 7. Start the chromedriver

…and tell it to allow connections from anywhere. This is accomplished, unintuitively, by using the `--whitelisted-ips` flag without any arguments.

```
> chromedriver --whitelisted-ipsStarting ChromeDriver 81.0.4044.69 (6813546031a4bc83f717a2ef7cd4ac6ec1199132-refs/branch-heads/4044@{#776}) on port 9515All remote connections are allowed. Use a whitelist instead!Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.
```

Maybe later we’ll figure out how to provide a whitelist for just the dev container.

## 8. Run system tests inside the container

```
$ PARALLEL\_WORKERS=1 rails test:system
```

We found parallel system tests to be flakey, so we disable them with an environment variable.

## 9. Enjoy spooky automated browser action!

![Animation of Rails system tests driving a browser](https://i2.wp.com/avdi.codes/wp-content/uploads/2020/05/711E376C-280B-4D9F-B64B-A9DCBFAC90A0.gif?fit=770%2C433&ssl=1)<figcaption> <img src="https://s.w.org/images/core/emoji/12.0.0-1/72x72/1f37f.png" alt="🍿" style="height: 1em; max-height: 1em;"><img src="https://s.w.org/images/core/emoji/12.0.0-1/72x72/1f37f.png" alt="🍿" style="height: 1em; max-height: 1em;"><img src="https://s.w.org/images/core/emoji/12.0.0-1/72x72/1f37f.png" alt="🍿" style="height: 1em; max-height: 1em;"> </figcaption>