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.

Screenshot 2022-11-07 at 18.01.28.png

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

Nov-07-2022 18-28-17.gif

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.