Question

I'm writing some feature tests for my Rails 4 app using rspec and capybara. I have it working properly, but I'm struggling to make sense of some aspects of testing this way.

I've been reading that each test (it "should ..." block) should test only 1 thing. Ok, that sounds great, but when I put that into practice I end up writing a ton of tests for simple things.

Let's say I have a standard signup form that takes email, password, and credit card.

So test write a feature test for my signup, do I have to write 3 separate tests to test the feature?

describe "Signup" do
  it "informs user of an invalid email" do
    visit signups_path
    fill_in "Email", with: ""
    click_button "Signup"
    expect(page).to have_text("Email can't be blank")
  end
  it "informs user of an invalid password" do
    visit signups_path
    fill_in "Email", with: "test@test.com"
    fill_in "Password", with: ""
    click_button "Signup"
    expect(page).to have_text("Password can't be blank")
  end
  it "informs user of an invalid credit card" do
    visit signups_path
    fill_in "Email", with: "test@test.com"
    fill_in "Password", with: "valid-password"
    fill_in "Card", with: "bogus"
    click_button "Signup"
    expect(page).to have_text("Card is invalid")
  end
end

It would seem simpler to test all of these in a single test. I'm struggling to find writing on the proper method of performing feature testing. I don't want to start randomly writing tests that (1) don't actually cover/test my code or (2) become bloated and slow because I was writing them incompetently. I understand the necessity of tests, just not sure how to best approach feature tests like the one above.

Was it helpful?

Solution

You should DRY up your code to minimize duplication. You can move directives you call at the beginning of each test to before, and extract helper method for the repeated code:

describe "Signup" do
  before do
    visit signups_path
  end

  def fill_form(fields = {})
    fields.each do |field_name, field_value|
      fill_in field_name.to_s.capitalize, with: field_value
    end
    click_button "Signup"
  end

  it "informs user of an invalid email" do
    fill_form email: ""

    expect(page).to have_text("Email can't be blank")
  end
  it "informs user of an invalid password" do
    fill_form email: "test@test.com", password: ""

    expect(page).to have_text("Password can't be blank")
  end
  it "informs user of an invalid credit card" do
    fill_form email: "test@test.com", 
              password: "valid-password",
              card: "bogus"

    expect(page).to have_text("Card is invalid")
  end
end

OTHER TIPS

When writing integration tests, it's important to avoid coupling the test to the UI.

Here's an example:

RSpec.feature 'Signup', test_helpers: [:sign_ups] do
  before { visit signups_path }

  it 'informs user of an invalid email' do
    sign_ups.sign_up_with(email: '')
    sign_ups.should.have_error("Email can't be blank")
  end

  it 'informs user of an invalid password' do
    sign_ups.sign_up_with(email: 'test@test.com')
    sign_ups.should.have_error("Password can't be blank")
  end

  it 'informs user of an invalid credit card' do
    sign_ups.sign_up_with(email: 'test@test.com', password: 'valid-password', card: 'bogus')
    sign_ups.should.have_error('Card is invalid')
  end
end

which you could implement using page objects, or a library like Capybara Test Helpers:

class SignUpsTestHelper < Capybara::TestHelper
  def sign_up_with(email:, password: nil, card: nil)
    fill_in 'Email', with: email
    fill_in 'Password', with: password if password
    fill_in 'Card', with: card if card
    click_button 'Signup'
  end

  def have_error(message)
    have_selector('#error_explanation', text: message)
  end
end

That said, sometimes full isolation is not needed, and combining the scenarios should make the test suite significantly faster:

RSpec.feature 'Signup', test_helpers: [:sign_ups] do
  before { visit signups_path }

  it 'informs user of invalid info' do
    sign_ups.sign_up_with(email: '')
    sign_ups.should.have_error("Email can't be blank")

    sign_ups.sign_up_with(email: 'test@test.com')
    sign_ups.should.have_error("Password can't be blank")

    sign_ups.sign_up_with(email: 'test@test.com', password: 'valid-password', card: 'bogus')
    sign_ups.should.have_error('Card is invalid')
  end
end

Notice that now that the test code is well encapsulated, it's still easy to follow in the condensed version.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top