How can I protect my Capybara tests against arbitr

2019-07-15 08:46发布

问题:

In general I like how Capybara works. I just find it hard to believe that it's really a good idea to codify user-visible text in the tests; e.g.

click_link('Delete', match: :first)

OK, so I write my app with tests like this, and then my designer comes through, or the brand/marketing person sees "Delete" and has a fit because we do not "delete" things, we "remove" them (or whatever - point is, it's arbitrary). So they go through and make arbitrary changes to the content of the buttons and links in my app, and now all my tests are broken even though the app works fine. They're not really coders, so it's not really their responsibility to know what to do with my tests, and really, I don't think my tests should care what text they choose to change. They shouldn't really care if I need to translate the whole shebang to some other language either.

What's a sane way to address this? I was thinking I could just add a CSS class to each testable element and look for that, but that seems likely to run afoul of the designer. Can I just add otherwise-meaningless metadata for my tests to cue on? Or am I looking at this the wrong way?

回答1:

1) The easiest way I think to use find method with id of element or with xpath and click() function:

Here is an example:

feature 'Shall check devise authorization' do

  scenario "shall authorize User with correct email and password" do
    user = User.find(1)
    visit new_user_session_path
    fill_in "user[email]", with: user.email
    fill_in "user[password]", with: user.password
      # click_button "Sign in"
    find('input#sign_in_button').click()
      # find("//input[@id='sign_in_button']").click()
    has_selector? 'input#created_at_first'
  end

end

Here click_button "Sign in" the same as

find('input#sign_in_button').click() 
[id of element] 

and also the same as

find("//input[@id='sign_in_button']").click()
[xpath]

Choose whatever you want.

If you would like to choose element by its class and id, here is the solution:

find('input#sign_in_button.btn.btn-primary.btn-success').click()
find("//input[@class='btn btn-primary btn-success'][@id='sign_in_button']").click()

Also it can be helpful to inspect page capybara#finding. There are a lot of useful information.

2) Another option is to use capybara#scripting.

But it is a little harder, because you need to configure your Capybara driver for testing JS.

To configure your Capybara driver you should add gem 'capybara-webkit' in your Gemfile (you need to have libqtwebkit-dev).

sudo apt-get install libqtwebkit-dev
bundle install 

Then place in your spec_helper.rb something like this:

Capybara.current_driver = :webkit
Capybara.run_server = true #Whether start server when testing
#Capybara.server_port = 3000
#Capybara.default_selector = :css #:xpath #default selector , you can change to :css
Capybara.default_wait_time = 5 #When we testing AJAX, we can set a default wait time
Capybara.ignore_hidden_elements = false #Ignore hidden elements when testing, make helpful when you hide or show elements using javascript
Capybara.javascript_driver = :webkit

and the following spec will work correctly:

# -*- encoding : utf-8 -*-
require_relative '../spec_helper'

feature 'Shall check devise authorization' do

  scenario "shall authorize User with correct email and password", js: true do
    user = User.find(1)
    visit new_user_session_path
    fill_in "user[email]", with: user.email
    fill_in "user[password]", with: user.password

    page.execute_script("$('#sign_in_button').click()")

    has_selector? 'input#created_at_first'
  end

end

The capybara-webkit driver is for true headless testing. It uses QtWebKit to start a rendering engine process. It can execute JavaScript as well. It is significantly faster than drivers like Selenium since it does not load an entire browser

For more details please visit test-javascript-with-capybara-webkit

UPDATE:

Also you can browse this spec from capybara source, which can help you to understand how you can find elements or add selectors:



回答2:

I usually address this problem by using locales instead of adding text directly into my views. For example:

# config/locales/en.yml
en:
  buttons:
    delete: &delete "Remove"
  my_model:
    index:
      delete: *delete
    show:
      delete: *delete

Then I can access the locale in my view. This erb code will render a button to delete my record with the text "Remove":

<!-- app/views/my_models/show.html.erb -->
<%= link_to t(".delete"), my_model_path(@my_model), method: :delete %>

I can use the translation helper in my specs to find the button. To do this, first I need to include the translation helper:

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include ActionView::Helpers::TranslationHelper
  # ...
end

# spec/features/my_model_spec.rb
RSpec.feature "MyModel", type: :feature  do
  scenario "clicking a delete button" do
    click_link(t("buttons.delete"), match: :first)
  end
end

The only trick now is teaching my designers how to use i18n which is its own challenge...