Docker + Rails + React + Selenium = 🔥

As a ruby developer, I like to write feature tests (or integration tests) using Capybara and Selenium.

VNC Viewer connecting to Selenium standalone debug Docker Compose container

This post is highly inspired by https://medium.com/@jfroom/docker-compose-capybara-selenium-standalone-for-dev-ci-6514bf16d77b I would recommend reading it first.

With few changes :

  • App (web) is a SPA (Single Page App) : React
  • Api is in JSON using ruby on rails
  • Db is in Mongodb

Note I have the following folder in my root repository :

.
..
api/ => Rails app
client/ => React app
data/ => Mongodb data files
docker-compose.yml
README.md

Api and client has their own Dockerfile

Quick overview :

  • Mongo runs for both environments dev & test
  • Development have http://api:3000 (rails) and http://app:3001 (react)
  • Test have http://test_api:3002 (rails), http://test_app:3001 (react) and firefox on 5900

Try building your image without error :

docker-compose build

Now it’s time to tell your your rails_helper.rb to connect to your selenium container’s firefox :

Capybara.default_driver = :remote_firefox
Capybara.default_max_wait_time = 5Capybara.register_driver :remote_firefox do |app|
 firefox_capabilities = Selenium::WebDriver::Remote::Capabilities.firefox()
 profile = Selenium::WebDriver::Firefox::Profile.new
 profile["intl.accept_languages"] =  "en-US"
 options = Selenium::WebDriver::Firefox::Options.new
 options.profile = profile
 Capybara::Selenium::Driver.new(app, browser: :remote,
   url: "http://#{ENV['SELENIUM_REMOTE_HOST']}:4444/wd/hub", desired_capabilities: firefox_capabilities)
end
Capybara.app_host = "http://test_app:3001"

Now you can run your test :

# specific test
docker-compose run test bundle exec rspec spec/features/...rb
# whole test suite
docker-compose up test

When you bring changes to your docker-compose.yml or dockerfile make sure to turn down properly :

docker-compose down

More…

What if I have also feature tests to run against my rails server ? (api…)

Baiscally you can use rspec metadata and switch Capybara.app_host properly exemple :

RSpec.configure do |config|
     # this force to boot rails app
     config.before(:suite) do
       puts "booting default server..."
       ip = `/sbin/ip route|awk '/scope/ { print $9 }'`
       ip = ip.gsub "\n", ""
       Capybara.server_port = "3000"
       Capybara.server_host = ip
       Capybara.current_session.server.boot
     endconfig.before(:each, type: :feature) do |example|
       I18n.locale = :fr #for date picker
       if example.metadata[:rails]
         Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}"
       else
         Capybara.app_host = "http://test_app:3001"
       end
     end
end

Now you can tag your spec that hit rails server instead of react :

feature ‘house’, :rails do
 scenario 'when creating new house' do
   visit '/' #will hit your spawn rails server
 end
end

Since my spec suite is dockerized I can use CircleCI right ?

Yeap, here is my config (I’m pretty sure it can be optimized, ex: I dont know why the cache doesnt work properly)

# Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
#
version: 2
jobs:
 build:
   machine:
     services:
       - docker
     environment:
       RAILS_ENV: test
   steps:
     # output information about the docker environment
     - checkout
     - run:
         command: docker info
     - run:
         command: cp ./api/.env.example ./api/.env- run:
         command: ls -la
     # build our Docker images
     - restore_cache:
         key: dependency-cache-{{ checksum "docker-compose.yml" }}-{{ checksum "./client/package.json" }}-{{ checksum "./api/Gemfile.lock" }}
     - run:
         name: "Build app and api image"
         command: docker-compose build app api
     - run:
         name: "Up test_app and test_api container"
         command: docker-compose up -d test_app test_api
     - save_cache:
         key: dependency-cache-{{ checksum "docker-compose.yml" }}-{{ checksum "./client/package.json" }}-{{ checksum "./api/Gemfile.lock" }}
         paths:
           - ./docker-build
     - run:
         name: run tests
         command: |
           mkdir /tmp/test-results
           TEST_FILES="$(circleci tests glob "api/spec/**/*_feature.rb"| circleci tests split --split-by=timings)"
           docker-compose run test bundle exec rspec \
                           --format RspecJunitFormatter \
                           --out /tmp/test-results/rspec.xml \
                           --format progress \
                           -- $(sed -e 's/\n/\\n/' -e 's/ /\ /' <<< "${TEST_FILES}")# collect reports
     - store_test_results:
         path: ~/project/api/tmp/test-results
     - store_artifacts:
         path: ~/project/api/tmp/test-results
         destination: test-results
     - store_artifacts:
         path: ~/project/api/tmp/capybara