Search code examples
ruby-on-railscapybaradryminitestwarden

How can I make tests DRY in Rails and still use Capybara and Warden? (using minitest)


My intention is to create a few tests for the same form, and in these tests I need Capybara to fill the form completely. Still, I wanted to avoid rewriting the code for filling the form.

I'm not using RSPEC! I'm using minitest.

The problem I'm facing is that the Capybara method visit and the Warden helper method login_as are not accessible from my CapybaraHelper module.

I've seen this question, so I created my module in the test/support folder, but the methods I mentioned still are not accessible. How to reuse code in Capybara

I've seen this question, but the code I want to reuse doesn't seem to fit well in the setup method nor in the teardown method. Rails: Premake a model instance for your unit tests (for DRY purposes)?

I've also seen posts saying this module should be inside test_helper.rb, but as I add more modules to this file it will get messy.

So now I'm wondering what I'm doing wrong. I've tried adding the following lines to CapybaraHelper but it didn't help. It actually raised the error NoMethodError: undefined methodsetup' for CapybaraHelper:Module`.

include Devise::Test::ControllerHelpers
include Warden::Test::Helpers

Is it the right way to reuse code in tests? Am I missing something that should be included in my helper module? All those methods work perfectly in the test controller that is using CapybaraHelper:Module.

Here is the error message:

NoMethodError: undefined method `login_as' for CapybaraHelper:Module

And here is an error message from another test using CapybaraHelper:Module.

NoMethodError: undefined method `visit' for CapybaraHelper:Module

Here's my test:

require 'test_helper'

class TravelsControllerTest < ActionController::TestCase
  include Devise::Test::ControllerHelpers
  include Warden::Test::Helpers
  Warden.test_mode!

  test 'should accept correct fields' do
    CapybaraHelper.login
    result = CapybaraHelper.access_and_fill_travel_page
    assert_equal "/travels/success/#{Travel.last.uuid}", result[:final_path]
  end

end

And here's the helper I created in test/support/capybara/capybara_helper.rb to avoid code duplication:

require 'test_helper'
require 'capybara/rails'
require 'capybara/poltergeist'

module CapybaraHelper
  def self.access_and_fill_travel_page options = {}
    options.symbolize_keys!
    set_js_driver
    visit(Rails.application.routes.url_helpers.root_path)
    initial_path = current_path
    #Fields
    fill_in('origin', with: options[:origin] || 'Guarulhos')
    fill_autocomplete('origin', with: options[:origin] || 'Guarulhos')
    fill_in('destination', with: options[:destination] || 'Seul')
    fill_autocomplete('destination', with: options[:destination] || 'Seul')
    fill_in('date_from', with: options[:date_from] || Date.today+10)
    fill_in('date_to', with: options[:date_to] || Date.today+26)
    fill_in('adults', with: options[:adults] || 1)
    fill_in('children', with: options[:children] || 0)
    fill_in('babies', with: options[:babies] || 0)
    find('#travel-submit').click()
    final_path = current_path
    return {initial_path: initial_path, final_path: final_path}
  end

  def self.fill_autocomplete(field, options = {})
    page.execute_script %Q{ el = $('input[name=#{field}]').get(0) }
    page.execute_script %Q{ $(el).trigger('focus') }
    page.execute_script %Q{ $(el).trigger('keydown') }
    page.all('.ui-menu-item', minimum: 1)
    page.execute_script %Q{ item = $('.ui-menu-item').get(0) }
    page.execute_script %Q{ $(item).trigger('mouseenter').click() }
  end

  def self.set_js_driver
    Capybara.javascript_driver = :poltergeist
    Capybara.current_driver = Capybara.javascript_driver
  end

  def self.login
    user = FactoryGirl.create(:user)
    login_as(user, :scope => :user)
  end

end

Solution

  • You should use ActionDispatch::IntegrationTest and not ActionController::TestCase as the parent class for tests using capybara. ActionController::TestCase mocks out the request phase and large parts of Rails. It is depreciated in Rails 5.

    Instead of calling methods on your test helper module you should mix them into your test classes.

    class TravelsIntegrationTest < ActionDispatch::IntegrationTest
      include Devise::Test::ControllerHelpers
      include Warden::Test::Helpers
      include CapybaraHelper 
      Warden.test_mode!
    
      test 'should accept correct fields' do
        login
        # ...
      end
    end
    
    module CapybaraHelper
      def login(user = FactoryGirl.create(:user))
        login_as(user, scope: :user)
      end
    end
    

    Apart from that you are lacking in code organization - setup steps like setting Warden.test_mode! should be done in your test_helper.rb not repeated across your tests. Don't throw all your step definitions into a single file either. You can place them in /test/support/ for example.

    module SessionHelper
      def login(user = FactoryGirl.create(:user))
        login_as(user, :scope => :user)
      end
    end
    
    module TravelHelper
      def access_and_fill_travel_page
        # ...
      end
    end
    

    And if you really want to keep it dry use inheritance to setup your test classes:

    class BaseIntegrationTest < ActionDispatch::IntegrationTest
      include Devise::Test::ControllerHelpers
      include Warden::Test::Helpers
      include SessionHelper 
    end
    
    class TravelsIntegrationTest < BaseIntegrationTest
      test 'should accept correct fields' do
        login
        # ...
      end
    end