Basic Testing with Feature Specs and Capybara

One of my goals was to create an RSpec feature spec for a functionality using StimulusReflex in my Rails app, but I wanted to test it out as a whole. That is, I click a button and the page updates appropriately. Unit tests can't do this, and I needed something that would test out not only the controllers but also the JavaScript. Enter feature specs and Capybara.

Even though this post is slightly geared towards StimulusReflex it should help anyone who is looking to implement complete front-to-back testing of their app.

How to Start

The basic shape of a feature spec should look something like this:

require 'rails_helper'

RSpec.feature "Creation management", :type => :feature do
  scenario "User clicks to create something" do
    visit "/" # go to a page
    click_link "Create" # do something
    expect(page).to have_text("Something was Created")
  end
end

It'll vary depending on how RSpec is configured, but the question is how do we turn this into something useful? Where does someone start to create feature specs that allows them to test their application?

The first step is confirm that the page you're visiting is your intended target. For example, it's easy to think that a page is being queried when it's really not and the login page is being served up. To figure this out binding.pry is going to be helpful. To start it should be added right after the line that visits the endpoint like so

    visit "/" # go to a page
    binding.pry
    click_link "Create" # do something

Once the test is run again the response can be inspected to verify it is what we think it is. page.html will spit out what we're visiting. If you want a better display use pretty print, pp page.html.

Logging In

One possibility is that the application is responding as if there isn't an authenticated user. To get around this we can log in. Depending on the tool used this could take different forms. For devise it can look something like this

  scenario "User clicks to create something" do
    user = create(:user)
    sign_in user
    visit "/"

However, before sign_in can be used the appropriate linkages must be made within the rails_helper.rb file. For features the following line under the Rspec.configure block should work.

config.include Devise::Test::IntegrationHelpers, type: :feature

Once the spec can log in and get a proper response it can then start manipulating the page.

Finding and Clicking a Link

It's most likely that the first error will be something along the lines of

Capybara::ElementNotFound:
  Unable to find link "#something"
  Note: It appears you may be passing a CSS selector or XPath expression rather than a locator. Please see the documentation for acceptable locator values.

The trick with this is that the locator can be an id or text of the link. Prepending the # for ids fouls this up. Remove it to get it to work.

The next problem that might happen is that Capybara will discover that there is more than one relevant element. In the case of ids there should be only one of each id on a page. If that's not the case that should be resolved before continuing.

JavaScript Won't Fire

At this point it might look like things are working, but are they? If the goal is to test that the frontend is communicating with the backend then we need to invoke JavaScript. The current state of the test won't do this. To get Capybara to use the Capybara.javascript_driver we'll need to tell the spec to do this with

RSpec.feature "Creation management", :type => :feature, js: true do

Doing this will allow Capybara to invoke JavaScript, but it will incur a performance hit so keep that in mind.

Webpacker Woes

My application has been through a few versions of Rails and I was having trouble with the JavaScript. On the front end, the specs would not recognize ActionCable. This meant that the calls to the backend over websockets weren't working. My hope was that if I updated webpacker, webpack and all the dependencies would just work. In troubleshooting all that there were a few things that I learned:

  • It looks like webpack 4 has dependencies that have vulnerabilities (e.g. glob-parent)
  • webpacker 5.4 doesn't support webpack 5
  • webpacker 6 does, but at the time of this post it was still in the release candidate phase
  • It looks like the Rails community might be moving away from webpack to jsbundling anyway

In the end I think this was the solution that worked for me. I had to drop my public/packs*/ directories and rebuild them with RAILS_ENV=test bundle exec rails webpacker:compile.

What to Expect

Now that Capybara is recognizing the link and is able to click it the next step is to set up the checks for the tests. Since I want to make sure StimulusReflex is updating something correctly the test will record the original value of a field, click the button that invokes the reflex, and then check the new value against what it was. This looks something like this.

    visit "/"
    sleep 0.01
    original_thing = find("#the_thing").find('a').text
    click_link "something"
    new_thing = find("#the_thing").find('a').text
    expect(new_thing).not_to eq original_thing

That's it. From here more specs can be created, but they should be limited to general operations that need to be functional. Testing specific edge cases should be left to unit tests that will run much faster.

Summary

It doesn't take much to create a feature test, and they are a great way to test functionality of an app at a system level. In the end the code looks something like this.

require 'rails_helper'

RSpec.feature "Creation management", :type => :feature, js: true do
  scenario "User clicks to create something" do
    user = create(:user)
    sign_in user
    create(:something, name: 'Widget') # populating the database with something
    visit "/" # go to a page
    sleep 0.01 # needed a small delay for the ActionCable channel (see below)
    original_thing = find("#the_thing").find('a').text
    click_link "Create" # do something
    new_thing = find("#the_thing").find('a').text
    expect(new_thing).not_to eq original_thing
  end
end

Unanswered Questions and Other Thoughts.

With ActionCable I needed to add a small delay so that the channel was ready for the click. if this didn't happen the front end would respond with "The ActionCable connection is not open! this.isActionCableConnectionOpen() must return true before calling this.stimulate()". To workaround this I added a sleep for 1/100th of a second. Ideally I'd like for the spec to wait for the connection to open before it attempts to test the subject of the test.

Another neat functionality would be to have the expect detect if a element changed so that the spec doesn't have to record the original value to compare against the post-action value.

Back