Configure GitHub Actions to work with rspec-rails

The next release of Rails will ship with a CI template which among other things, will run the Rails test suite. I figured this would be a useful addition for the next release of Suspenders, so I lifted the template and modified it to work with rspec-rails.

It was a mostly straightforward process, but I wanted to highlight some of the issues I ran into.

Our Base

Below is a distilled Gemfile. It assumes we ran rails new with the --skip-test flag, since we’re choosing to use rspec-rails.

rails new <app_name> \
--skip-test \
-d=postgresql

Because we skipped scaffolding the test files, we’ll need to add capybara and selenium-webdriver too.

# Gemfile

group :development, :test do
  gem "rspec-rails", "~> 6.1.0"
end

group :test do
  gem "capybara"
  gem "selenium-webdriver"
end

Then we can run RSpec’s installation script to generate the necessary files.

bin/rails g rspec:install

Finally, let’s create a simple system test so we have something to use in CI.

require 'rails_helper'

RSpec.describe "Homepage", type: :system do
  it "passes" do
    visit root_path

    expect(page).to have_content "Welcome!"
  end

  it "fails (and takes a screenshot)" do
    visit root_path

    expect(page).to_not have_content "Welcome!"
  end
end

You’ll note we have a test that’s designed to fail. This is deliberate, since we want to ensure failure screenshots are captured in CI.

Building the CI script

Since it’s likely a future commit will remove the test job from the CI template when rails new is passed the --skip-test flag, we can’t just count on it existing. For now, we can just copy the CI template locally into .github/workflows/ci.yml, and adjust it accordingly.

Run specs not tests

The first thing we’ll want to do is update the script to run spec and not test test:system, since those commands do not exist.

--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,7 +79,7 @@ jobs:
         env:
           RAILS_ENV: test
           # REDIS_URL: redis://localhost:6379/0
-        run: bin/rails db:setup test test:system
+        run: bin/rails db:setup spec

       - name: Keep screenshots from failed system tests
         uses: actions/upload-artifact@v4

Keep parity with ApplicationSystemTestCase

Next, we’ll want to configure RSpec to use :headless_chrome by default, since this is the new default in Rails. This is important because our CI script will error out, since RSpec defaults to :selenium. However, a future release of rspec-rails will change the default to keep parity with Rails.

# spec/support/driver.rb

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
  end
end

I also felt it was important to keep parity with the screen_size too, but that part is optional.

Save screenshots of failed system tests

Rails automatically takes a screenshot when a system test fails. We can test this by running our failing test locally.

Failures:

  1) Homepage fails (and takes a screenshot)
     Failure/Error: expect(page).to_not have_content "Welcome!"
       expected not to find text "Welcome!" in "Welcome!"

     [Screenshot Image]: /tmp/capybara/failures_r_spec_example_groups_homepage_fails_-and_takes_a_screenshot-_632.png


     # ./spec/system/homepage_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 9.43 seconds (files took 1.02 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/system/homepage_spec.rb:10 # Homepage fails (and takes a screenshot)

However, the path it saves to differs from the path set in the CI template. If the path is incorrect, the screenshots won’t be available in CI. We can adjust this as follows:

--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,5 +86,5 @@ jobs:
         if: failure()
         with:
           name: screenshots
-          path: ${{ github.workspace }}/tmp/screenshots
+          path: ${{ github.workspace }}/tmp/capybara
           if-no-files-found: ignore

Wrapping up

Below is what the final test portion of the modified CI script looks like. Fortunately, the next release of Suspenders will generate this file for us, but I’m sharing it here for added transparency.

name: CI

on:
  pull_request:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
      - name: Install packages
        run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client libpq-dev

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version
          bundler-cache: true

      - name: Run tests
        env:
          RAILS_ENV: test
          DATABASE_URL: postgres://postgres:postgres@localhost:5432
        run: bin/rails db:setup spec

      - name: Keep screenshots from failed system tests
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: screenshots
          path: ${{ github.workspace }}/tmp/capybara
          if-no-files-found: ignore