Ruby-Rspec: Initialize PageObjects once in spec_helper instead of each spec - ruby

I variables set for xpaths in a file called PageObjects. Each spec I run I initialize the page objects with "p = PageObjects.new". However, I would like to initialize "p = PageObjects.new" once in "spec_helper.rb" instead of each spec.
This still gives me "error: uninitialized constant PageObject"...
require 'selenium-webdriver'
require 'yaml'
require 'rspec/retry'
require 'pry'
require 'bundler/setup'
p = PageObject.new
RSpec.configure do |config|
config.default_sleep_interval = 1
config.default_retry_count = 4
config.verbose_retry = false
config.display_try_failure_messages = true
config.exceptions_to_retry = [Net::ReadTimeout, Capybara::ElementNotFound]
end
Is there a way to achieve my goal by initializing PageObject once inside spec_helper rather than in each spec?

RSpec helpers seems to be the perfect solution for you
define the helper.rb
module Helpers
def p
#page_object ||= PageObject.new
end
end
Configure RSpec to include it:
RSpec.configure do |c|
c.include Helpers
end
And then you can use p method that will give you the PageObject:
specify do
expect(p).to be_a(PagObject)
expect(p.object_id).to eq(p.object_id)
end

You effectively want your test database to be maintained between tests. This is dangerous for a number of reasons, the most obvious being previous tests will affect future ones. As you're dealing with the same PageObject you will need to reset it between tests.
Putting that to one side, the options for enabling / disabling this can be found at:
https://relishapp.com/rspec/rspec-rails/docs/transactions, namely:
When you run rails generate rspec:install, the spec/rails_helper.rb
file includes the following configuration:
RSpec.configure do |config|
config.use_transactional_fixtures = true
end
The name of this setting is a bit misleading. What it really means
in Rails is "run every test method within a transaction." In the
context of rspec-rails, it means "run every example within a
transaction."
The idea is to start each example with a clean database, create
whatever data is necessary for that example, and then remove that data
by simply rolling back the transaction at the end of the example.
Disabling transactions If you prefer to manage the data yourself, or
using another tool like database_cleaner to do it for you, simply tell
RSpec to tell Rails not to manage transactions:
RSpec.configure do |config|
config.use_transactional_fixtures = false
end

Related

poltergeist doesn't seem to wait for phantomjs to load in capybara

I'm trying to get some rspec tests run using a mix of Capybara, Selenium, Capybara/webkit, and Poltergeist. I need it to run headless in certain cases and would rather not use xvfb to get webkit working. I am okay using selenium or poltergeist as the driver for phantomjs. The problem I am having is that my tests run fine with selenium and firefox or chrome but when I try phantomjs the elements always show as not found. After looking into it for a while and using page.save_screenshot in capybara I found out that the phantomjs browser wasn't loaded up when the driver told it to find elements so it wasn't returning anything. I was able to hack a fix to this in by editing the poltergeist source in <gem_path>/capybara/poltergeist/driver.rb as follows
def visit(url)
if #started
sleep_time = 0
else
sleep_time = 2
end
#started = true
browser.visit(url)
sleep sleep_time
end
This is obviously not an ideal solution for the problem and it doesn't work with selenium as the driver for phantomjs. Is there anyway I can tell the driver to wait for phantom to be ready?
UPDATE:
I was able to get it to run by changing where I included the Capybara::DSL. I added it to the RSpec.configure block as shown below.
RSpec.configure do |config|
config.include Capybara::DSL
I then passed the page object to all classes I created for interacting with the webpage ui.
An example class would now look like this
module LoginUI
require_relative 'webpage'
class LoginPage < WebPages::Pages
def initialize(page, values = {})
super(page)
end
def visit
browser.visit(login_url)
end
def login(username, password)
set_username(username)
set_password(password)
sign_in_button
end
def set_username(username)
edit = browser.find_element(#selectors[:login_edit])
edit.send_keys(username)
end
def set_password(password)
edit = browser.find_element(#selectors[:password_edit])
edit.send_keys(password)
end
def sign_in_button
browser.find_element(#selectors[:sign_in_button]).click
end
end
end
Webpage module looks like this
module WebPages
require_relative 'browser'
class Pages
def initialize(page)
#page = page
#browser = Browser::Browser.new
end
def browser
#browser
end
def sign_out
browser.visit(sign_out_url)
end
end
end
The Browser module looks like this
module Browser
class Browser
include Capybara::DSL
def refresh_page
page.evaluate_script("window.location.reload()")
end
def submit(locator)
find_element(locator).click
end
def find_element(hash)
page.find(hash.keys.first, hash.values.first)
end
def find_elements(hash)
page.find(hash.keys.first, hash.values.first, match: :first)
page.all(hash.keys.first, hash.values.first)
end
def current_url
return page.current_url
end
end
end
While this works I don't want to have to include the Capybara::DSL inside RSpec or have to include the page object in the classes. These classes have had some things removed for the example but show the general structure. Ideally I would like to have the Browser module include the Capybara::DSL and be able to handle all of the interaction with Capybara.
Your update completely changes the question so I'm adding a second answer. There is no need to include the Capybara::DSL in your RSpec configure if you don't call any Capybara methods from outside your Browser class, just as there is no need to pass 'page' to all your Pages classes if you limit all Capybara interaction to your Browser class. One thing to note is that the page method provided by Capybara::DSL is just an alias for Capybara.current_session so technically you could just always call that.
You don't show in your code how you're handling any assertions/expectations on the page content - so depending on how you're doing that you may need to include Capybara::RSpecMatchers in your RSpec config and/or your WebPages::Pages class.
Your example code has a couple of issues that immediately pop out, firstly your Browser#find_elements (assuming I'm reading your intention for having find first correctly) should probably just be
def find_elements(hash)
page.all(hash.keys.first, hash.values.first, minimum: 1)
end
Secondly, your LoginPage#login method should have an assertion/expectation on a visual change that indicates login succeeded as its final line (verify some message is displayed/logged in menu exists/ etc), to ensure the browser has received the auth cookies, etc before the tests move on. What that line looks like depends on exactly how you're architecting your expectations.
If this doesn't answer your question, please provide a concrete example of what exactly isn't working for you since none of the code you're showing indicates any need for Capybara::DSL to be included in either of the places you say you don't want it.
Capybara doesn't depend on visit having completed, instead the finders and matchers will retry up to a specified period of time until they succeed. You can increase this amount of time by increasing the value of Capybara.default_max_wait_time. The only methods that don't wait by default are first and all, but can be made to wait/retry by specifying any of the count options
first('.some_class', minimum: 1) # will wait up to Capybara.default_max_wait_time seconds for the element to exist on the page.
although you should always prefer find over first/all whenever possible
If increasing the maximum wait time doesn't solve your issue, add an example of a test that fails to your question.

Rails not loading in rspec file gives error "NameError: uninitialized constant" when accessing a model

Rails rails 4.0.2 and Ruby 2.0.0
I am using watir-webdriver with rspec to test my web app. I want to pick the last item in a list where presence of an associated model is true. Basically the model on the page is Category, and in the list of categories I want to select the last one that has Tools associated with it. A Category has many Tools. My spec file:
require 'watir'
require 'watir-webdriver'
browser = Watir::Browser.new
browser.window.maximize
RSpec.configure do |config|
config.before(:each) { #browser = browser }
config.after(:suite) { browser.close unless browser.nil? }
end
url = 'localhost:3000'
serial = Time.now
describe 'it should log in and CRUD menu items' do
it 'should log in' do
#browser.goto url
#browser.text_field(id: 'username').set Logins::user
#browser.text_field(id: 'password').set Logins::password(url)
#browser.button(:text, 'Login').click
#browser.link(:text, 'Menu Settings').click
end
it 'should not delete a category with tools still associated with it' do
category_name = Category.joins(:tools).where(true).order(name: :asc).last
#browser.link(:text, category_name).click
#browser.h4(class: 'tool_list')
#browser.link(text: 'Delete').click
#browser.driver.switch_to.alert.accept
#browser.p(text: 'You can not destroy this menu item until it is empty').present?
end
end
The line that is giving me the error is:
category_name = Category.joins(:tools).where(true).order(name: :asc).last
Not sure why I can't do a simple query in my spec file. I think I'm missing something really obvious.
edit
I updated my rspec version from 2 to 3 and ran:
rails generate rspec:install
because I was missing the spec/rails_helper.rb file. Still getting the same errors.
The problem isn't in Watir, but perhaps you have to require the file how defined your model ...
I think, perhaps, you have to require needed gems; like Active Record,
it's configuration, and initialisations files.

Setting a variable once for each time rspec is run

I am writing a Ruby gem that accesses the features of a web based API. I need to set up an object which will be initialized and log into the API just once for each time the tests are run. before(:all) is still excessive because it will run once for every describe block, and what I want is something that universally sets up once for all of the test files.
UPDATE
Just as a follow up, to make the object I was using available in the tests, I had to add a setting to the rspec config like this
config.add_setting :client
config.before(:suite) do
RSpec.configuration.client = TDAmeritradeApi::Client.new
RSpec.configuration.client.login
end
And then in the describe blocks I do this:
let(:client) { RSpec.configuration.client }
I believe you are looking for before(:suite) and you can use it in the config section of your spec_helper.rb.
RSpec.configure do |config|
config.before(:suite) do
# API login
end
end
You can use before(:suite) to run a block of code before any example groups are run. This should be declared in RSpec.configure
Source: http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/Hooks

How to set a "base URL" for Webrat, Mechanize

I would like to specify a base URL so I don't have to always specify absolute URLs. How can I specify a base URL for Mechanize to use?
To accomplish the previously proffered answer using Webrat, you can do the following e.g. in your Cucumber env.rb:
require 'webrat'
Webrat.configure do |config|
config.mode = :mechanize
end
World do
session = Webrat::Session.new
session.extend(Webrat::Methods)
session.extend(Webrat::Matchers)
session.visit 'http://yoursite/yourbasepath/'
session
end
To make it more robust, such as for use in different environments, you could do:
ENV['CUCUMBER_HOST'] ||= 'yoursite'
ENV['CUCUMBER_BASE_PATH'] ||= '/yourbasepath/'
# Webrat
require 'webrat'
Webrat.configure do |config|
config.mode = :mechanize
end
World do
session = Webrat::Session.new
session.extend(Webrat::Methods)
session.extend(Webrat::Matchers)
session.visit('http://' + ENV['CUCUMBER_HOST'] + ENV['CUCUMBER_BASE_PATH'])
session
end
Note that if you're using Mechanize, Webrat will also fail to follow your redirects because it won't interpret the current host correctly. To work around this, you can add session.header('Host', ENV['CUCUMBER_HOST']) to the above.
To make sure the right paths are being used everywhere for visiting and matching, add ENV['CUCUMBER_BASE_PATH'] + to the beginning of your paths_to method in paths.rb, if you use it. It should look like this:
def path_to(page_name)
ENV['CUCUMBER_BASE_PATH'] +
case page_name
Apologies if anyone got a few e-mails from this -- I originally tried to post as a comment and Stack Overflow's irritating UI got the better of me.
For Mechanize, the first URL you specify will be considered the base URL. For example:
require "rubygems"
require "mechanize"
agent = Mechanize.new
agent.get("http://some-site.org")
# Subsequent requests can now use the relative path:
agent.get("/contact.html")
This way you only specify the base URL once.

How do I get cucumber and pickle working with mongo_mapper, machinist, and machinist_mongo?

I would like to get machinist, machinist_mongo, mongo_mapper, cucumber and pickle to play nice together.
Currently I have machinist with all my blueprints configured and am using cucumber to do BDD. So far so good. My problem is I am having to write custom cucumber steps for all of my machinist blueprints. It is not really a problem per se, since it is not stopping me in my tracks, but as a .NET dev checking out rails, it feels really dirty to have to write a step for each blueprint whereas in .NET I could probably use reflection.
Is there any way I can get pickle's built in capture_model, capture_plural_factory, etc, to recognize my machinist blueprints?
I am pretty confident I have machinist configured and set up correctly, because when I use blueprintname.make, in a custom cucumber step, everything works out correctly.
Gem versions:
rails 2.3.8
cucumber 0.8.3
cucumber-rails 0.3.2
mongo 1.0.5
mongo_mapper 0.8.2
pickle 0.3.0
machinist 1.0.6
machinist_mongo 1.1.1
features/support/pickle.rb:
require 'pickle/world'
Pickle.configure do |config|
config.adapters = [:machinist]
end
I tried using config.adapters = [:machinist, Machinist::MongoMapperAdapter] but I get an error stating that there is no method factories for Machinist::MongoMapperAdapter.
undefined method `factories' for Machinist::MongoMapperAdapter:Class (NoMethodError) /usr/local/lib/ruby/gems/1.8/gems/pickle-0.3.0/lib/pickle/config.rb:25:in `factories'
features/support/machinist.rb:
require 'machinist'
require 'machinist/mongo_mapper'
require "#{Rails.root}/spec/blueprints"
require 'database_cleaner'
Before { Sham.reset } # reset Shams in between scenarios
spec/blueprints.rb (truncated for clarity)
require 'sham'
require 'faker'
Sham.code { Faker::Lorem.words 1 }
AccessCode.blueprint do
code
end
app/models/access_code.rb
class AccessCode
include MongoMapper::Document
key :code, String, :required => true
end
After days of beating my head against the wall, I have everything mostly working (I say mostly working because I'm not sure if there is something wrong that I haven't discovered yet). The fix was actually pretty simple once I figured it out.
To resolve the issue, and get my cucumber steps working with pickle, I changed MongoMapper::Document to include Pickle::Adapter::Base. I used lib/pickle/adapters/active_record.rb and data_mapper.rb (same path as active_record.rb) that come with pickle as an example. I did still need machinist_mongo, presumably to hook up pickle to my machinist blueprints.
I can't take credit for the code in def self.model_classes - it is stolen from tjtuom's pickle fork.
PS. If this is the completely wrong way to do it, please feel free to criticize or give suggestions, I am a complete ruby noob.
module MongoMapper::Document
module PickleAdapter
include Pickle::Adapter::Base
def self.model_classes
##model_classes ||= ::MongoMapper::Document.descendants.to_a +
::MongoMapper::Document.descendants.map { |klass| klass.subclasses }.flatten
end
# get a list of column names for a given class
def self.column_names(klass)
klass.column_names
end
# Get an instance by id of the model
def self.get_model(klass, id)
klass.find(id)
end
# Find the first instance matching conditions
def self.find_first_model(klass, conditions)
klass.find(:first, :conditions => conditions)
end
# Find all models matching conditions
def self.find_all_models(klass, conditions)
klass.find(:all, :conditions => conditions)
end
end
end
Set up pickle for machinist:
Pickle.configure do |config|
config.adapters = [:machinist]
end
To configure database_cleaner for mongo:
require 'database_cleaner'
require 'database_cleaner/cucumber'
DatabaseCleaner.orm = 'mongo_mapper'
DatabaseCleaner.strategy = :truncation

Resources