Level up your Capybara acceptance tests using SitePrism
TL;DR
If you struggle with maintaining brittle capybara tests that mix high-level verifications with low-level CSS selectors, you should consider using SitePrism, a semantic DSL for describing your web application.
You'll never have to hardcode CSS selectors in your acceptance tests ever again, making the acceptance tests easier to write, maintain and understand.
Tell me more
For the rest of the article, I assume you are familiar with Ruby on Rails, RSpec and Capybara
In the following examples, we will go through the process of adding new simple features to a basic application, where clients can place orders to buy large quantities of sand.
We want to make it as simple as possible for the clients to add new orders. Forms should be interactive, with field-level validations, providing inline validation errors when invalid values are being filled in.
I won't go into all the details about how those features were implemented, do not hesitate to leave a comment if you have any questions about the implementation.
What we will be focusing on is how :
- to write the acceptance tests using Capybara
- to refactor those tests using SitePrism, a semantic DSL for describing your web application.
To keep it simple, let's say our application allows users to order large quantities of sand. We want to test the form allowing the users to place orders. The form is very simple and it has, at least for now, only one field named quantity
.
Testing the form field
Let's assume that the client
- accepts orders in full metric tons (50 metric tons is valid, 50.1 metric tons is invalid)
- wants to discourage orders for less than 5 tons of sand by displaying a warning message
In addition, our client wants validation errors displayed on the fly, without the user having to submit the form.
Implementing the acceptance test for these requirements would look like this
require 'rails_helper'
RSpec.describe Order, type: :feature do
context 'create form' do
it "validates the form" do
# navigate to form
visit '/orders'
click_link('Add Order')
# accept only numbers
find(:css, "#order_quantity").fill_in with: 'X', fill_options: { clear: :backspace }
expect(
page.find_by_id("order_quantity__error_message")
).to have_text("Quantity is not a number")
expect(page).not_to have_css("#order_quantity__warning_message")
# accept only integers
find(:css, "#order_quantity").fill_in with: '50.1', fill_options: { clear: :backspace }
expect(
page.find_by_id("order_quantity__error_message")
).to have_text("Quantity must be an integer")
expect(page).not_to have_css("#order_quantity__warning_message")
# warning about small quantities
find(:css, "#order_quantity").fill_in with: '3', fill_options: { clear: :backspace }
expect(page).not_to have_css("#order_quantity__error_message")
expect(
page.find_by_id("order_quantity__warning_message")
).to have_text("Please avoid orders of less than 5 metric tons")
# valid order
find(:css, "#order_quantity").fill_in with: '99', fill_options: { clear: :backspace }
expect(page).not_to have_css("#order_quantity__error_message")
expect(page).not_to have_css("#order_quantity__warning_message")
# submit order & check the confirmation message
click_button("Create Order")
expect(page).to have_text("Order was successfully created")
end
end
end
This does the job, but it isn't pleasant. Having low level CSS selectors mixed with high-level acceptance checks will be hard to maintain in the long run.
Refactoring the tests using site prism
This is where the site_prism gem comes into action. By describing our orders page and orders create form using the DSL provided by the gem, we could rewrite the acceptance test in the following way :
require 'rails_helper'
RSpec.describe Order, type: :feature do
context 'create form' do
it "validates the form" do
# navigate to form
app.orders_page.load
app.orders_page.add_button.click
expect(app.order_new_form).to be_loaded
# accept only numbers
form = app.order_new_form
form.quantity.input.fill_in with: 'X', fill_options: { clear: :backspace }
expect(form.quantity.error).to have_text("Quantity is not a number")
expect(form.quantity).not_to have_warning
# accept only integers
form.quantity.input.fill_in with: '50.1', fill_options: { clear: :backspace }
expect(form.quantity.error).to have_text("Quantity must be an integer")
expect(form.quantity).not_to have_warning
# warning about small quantities
form.quantity.input.fill_in with: '3', fill_options: { clear: :backspace }
expect(form.quantity).not_to have_error
expect(form.quantity.warning).to have_text("Please avoid orders of less than 5 metric tons")
# valid order
form.quantity.input.fill_in with: '99', fill_options: { clear: :backspace }
expect(form.quantity).not_to have_error
expect(form.quantity).not_to have_warning
# submit order & check the confirmation message
form.submit.click
expect(app.orders_page).to be_loaded
expect(app.orders_page.flash_notice).to have_text("Order was successfully created")
end
end
end
Below are the pieces of code that made this possible.
First of all, we added the site_prism gem to the Gemfile.
# Gemfile
group :test do
gem 'site_prism'
end
I'm using rspec to test this app, so we need to add the following lines to spec/spec_helper.rb
file
require 'site_prism'
require 'site_prism/all_there' # Optional but needed to perform more complex matching
Next, we need descrive the orders page and the order create form page using the DSL provided by site_prism
# spec/objects_pages/orders/index_page.rb
class Pages::Orders::IndexPage < SitePrism::Page
set_url("/orders")
element :flash_notice, '.alert.alert-primary'
element :add_button, 'a', text: 'Add Order'
end
# spec/objects_pages/orders/new_form.rb
class Pages::Orders::NewForm < SitePrism::Page
set_url("/orders/new")
section :quantity , "#order_quantity__wrapper" do
element :input, "#order_quantity"
element :warning, "#order_quantity__warning_message"
element :error, "#order_quantity__error_message"
end
element :submit, 'input[type="submit"]'
end
One last step, is to facilitate the access the the pages from the feature tests.
# spec/objects_pages/app.rb
class App
def orders_page
Pages::Orders::IndexPage.new
end
def order_new_form
Pages::Orders::NewForm.new
end
end
For that purpose can use a shared context for all feature tests:
# spec/spec_helper.rb
RSpec.shared_context "site_prism" do
let(:app) { App.new }
end
RSpec.configure do |config|
config.include_context "site_prism", type: :feature
end
Conclusion
In this article, we barely scratched the surface of what is possible to do with site_prism.
The main takeaway is that by describing your application's pages using the DSL provided by site prism, tests are easier to write, maintain and understand.
I plan to explore in a future article the more advanced usages that can be done with site_prism.
Subscribe and you will be notified when new articles are published on this blog.
Subscribe to my newsletter
Read articles from Dorian Lupu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by