Capybara & Rspec

Matestack apps, pages and components can be tested with various test setups. We're using Rspec and Capybara a lot when creating apps with Matestack or work on Matestack's core itself and want to show you some basic elements of this setup.

We will show you how to setup a headless chrome for testing, because a headless browser approach gives you performance benefits and is better suited to be integrated in a CI/CD pipeline.

Setup

In this guide we assume that you know the basics of Rspec and Capybara and have both gems installed. If not, please read the basics about these tools here:

Note: Make sure you set config.action_controller.allow_forgery_protection = true in your `config/environments/test.rb' file.

Additionally you need a Chrome browser installed on your system.

We recommend to configure Capybara in a separate file and require it in your rails_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
require "spec_helper"
ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../config/environment", __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require "rspec/rails"

Dir[File.join File.dirname(__FILE__), "support", "**", "*.rb"].each { |f| require f }
# Add additional requires below this line. Rails is not loaded until this point!

Writing basic specs

Imagine having implemented a Matestack page like:

class SomePage < Matestack::Ui::Page

  def response
    plain "hello world!"
  end
  
end

A spec might look like this:

require "rails_helper"

describe "Some Page", type: :feature do
  
  it "should render hello world" do
    visit some_page_path
    expect(page).to have_content("hello world!")
  end

end

and then run this spec with bundle exec rspec spec/features/hello_world_spec.rb

This should start a webserver and trigger the headless chrome to request the specified page from it. Just like Capybara is working.

Testing asynchronous features

Above, we just tested a static "hello world" rendering and didn't use any JavaScript based functionality. We need to activate the JavaScript driver in specs where Matestack's built-in (or your own) JavaScript is required.

Let's add some basic built-in reactivity of Matestack, which requires JavaScript to work:

class SomePage < Matestack::Ui::Page

  def response
    onclick emit: "show_hello" do
      button "click me"
    end
    async show_on: "show_hello", id: "hello" do
      plain "hello world!"
    end
  end
  
end

The spec could look like this: Note that you now have to add the js: true on line 3!

require "rails_helper"

describe "Some Page", type: :feature, js: true do
  
  it "should render hello world after clicking on a button" do
    visit some_page_path
    expect(page).not_to have_content("hello world!")
    click "click me"
    expect(page).to have_content("hello world!")
  end

end

Capybara by default will wait for 2000ms before failing on an expectation. expect(page).to have_content("hello world!") therefore may take up to 2000ms to become truthy without breaking the spec. Following the documentation of Capybara, you can adjust the default wait time or set it individually on specific expectations. This built-in wait mechanism is especially useful when working with features requiring client-server communication, like page transitions, form or action submissions!

Testing forms and actions

Imagine a matestack_form used for creating new User ActiveRecord Model instances:

class SomePage < Matestack::Ui::Page

  def response
    matestack_form form_config do
      form_input key: :name, type: :text, label: "Name"
      button "submit me", type: :submit
    end
    toggle show_on: "succeeded" do
      plain "succeeded!"
    end
    toggle show_on: "failed" do
      plain "failed!"
    end
  end
  
  def form_config
    {
      for: User.new,
      path: users_path,
      method: :post,
      success: { emit: "succeeded" },
      failure: { emit: "failed" }
    }
  end
  
end

The according spec might look like this:

require "rails_helper"

describe "Some Page", type: :feature, js: true do
  
  it "should render hello world" do
    visit some_page_path
    fill_in "Name", with: "Foo"
    click "submit me"
    
    expect(page).to have_content("succeeded!")
  end

end

If you want to test if the User model was correctly saved in the Database, you could do something like this:

describe "Some Page", type: :feature, js: true do
  
  it "should render hello world" do
    visit some_page_path
    fill_in "Name", with: "Foo"
    
    expect {
      click "submit me"
      expect(page).to have_content("succeeded!") #required to work properly!
    }.to change { User.count }.by(1)
    
    # from here on, we know for sure that the form was submitted
    expect(User.last.name).to eq "Foo"
  end

end

Beware of the timing trap!

Without adding expect(page).to have_content("succeeded!") after click "submit me" the spec would fail. The User.count would be executed too early! You somehow need to use Capybara's built-in wait mechanisim in order to identify a successful asynchronous form submission. Otherwise the spec would just click the submit button and immediately expect a database state change. Unlike Capybara, plain Rspec expectations do not wait a certain amount of time before failing! Gems like https://github.com/laserlemon/rspec-wait are trying to address this issue. In our experience, you're better of using Capybara's built-in wait mechanism like shown in the example, though.

Above described approaches and hints apply for actions as well!

Debugging specs

When running specs in a headless browser, you're loosing insights on what exactly happens when a spec is failing. You have a simple yet powerful option to overcome this issue:

As described within the Setup section, it's possible to tell Capybara, which port should be used by the webserver while executing the specs. (By default it's randomly chosen on every spec run). When adding a simple sleep after a visit in your spec, you can request the same page, your spec would visit in you local browser and review what's going on there by manually executing the steps your spec would perform while reviewing the DOM and browser debugging tools:

describe "Some Page", type: :feature, js: true do
  
  it "should render hello world" do
    visit some_page_path
    
    p some_page_path # see the resolved URL string, copy to your browser
    sleep # add the sleep after the visit
    
    fill_in "Name", with: "Foo"
    click "submit me"
    
    expect(page).to have_content("succeeded!")
  end

end

Execute the spec and then visit the logged path in your local browser via localhost:33123/xyz for example.

This approach is especially useful when using factories in order to create temporary test data which is only accessible in your test ENV and that specific spec. In other words: you can review the test state way better compared to perform the spec steps in your local development ENV.

Last updated