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.
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
.
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.
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.
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.
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:
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
.
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.
ruby
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.
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. ```ruby require 'rails_helper'
RSpec.feature "Creation management", :type => :feature, js: true do scenario "User clicks to create something" do user = create(:user) signin 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) originalthing = find("#thething").find('a').text clicklink "Create" # do something newthing = find("#thething").find('a').text expect(newthing).notto eq original_thing end end ```
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