Last week, I was pairing with a coworker, and we came across a innocuous helper method that caused a 5x slow down in our feature specs (we are using rspec with capybara-webkit). With one tiny change, a test suite that took over 2.5 minutes to run now completes in less than 30 seconds.

Here is the method that was our performance bottleneck. Can you spot the problem?

def fill_in_address(address={})
  fill_in 'Street', with: address[:street]
  fill_in 'City', with: address[:city]
  if has_field?('State')
    fill_in 'State', with: address[:state]
  end
  fill_in 'Zip', with: address[:zip]
end

In our app, business rules dictate that the state field is not editable in some scenarios, so we disable it. Capybara doesn't find disabled fields with the #fill_in method.

If you use #fill_in on a disabled field,

feature "Filling in form", js: true do
  scenario "with disabled field" do
    visit "/addresses/new?disabled=true"
    fill_in 'State', with: 'TX'
  end
end

you will get the following error:

  1) Filling in form with disabled field
     Failure/Error: fill_in 'State', with: 'TX'
     Capybara::ElementNotFound:
       Unable to find field "State"

To fix this error, you can use the #has_field? method to check if the field exists and is not disabled before attempting to fill it in.

  scenario "with check for disabled field" do
    visit "/addresses/new?disabled=true"
    if has_field?('State')
      fill_in 'State', with: 'TX'
    end
  end

This works, but we are using the webkit asnychronous javascript driver with a default wait time of 5 seconds. If we run our spec with the profile option, rspec -p, we can see that these specs are taking over 5 seconds to complete.

Top 2 slowest examples (11.81 seconds, 100.0% of total time):
  Filling in form with disabled field
    6.75 seconds ./spec/features/form_spec.rb:10
  Filling in form with check for disabled field
    5.06 seconds ./spec/features/form_spec.rb:15

What's going on here? Because we are using the webkit driver, capybara will wait the full 5 seconds for the element to appear before concluding that it does not exist. If the element is not currently on the page, it may appear in a second or two in response to an AJAX request.

After searching through the capybara docs for a few minutes, we were able to come up with the following solution that doesn't make capybara wait.

  scenario "with check for disabled field - a better way" do
    visit "/addresses/new?disabled=true"
    field = find('[name*=state]')
    unless field.disabled?
      fill_in 'State', with: 'TX'
    end
  end

If we profile this example, we can see that it completes in a little over 1/10th of a second. That's a huge difference.

  Filling in form with check for disabled field
    5.09 seconds ./spec/features/form_spec.rb:15
  Filling in form with check for disabled field - a better way
    0.11892 seconds ./spec/features/form_spec.rb:22

Normally, I don't like to use CSS selectors in my feature specs because they do not communicate at the appropriate abstraction level. However, this check was in a heavily used helper method. The speedup was worth it, and the implementation details are hidden from the feature specs themselves.