Use helper methods in rspec before :suite block - ruby

A common pattern for using helper methods in rspec would be something like:
# spec/spec_helper.rb
Dir[File.expand_path(File.join('..', 'support', '**', '*.rb'), __FILE__)].each { |f| require f }
###
# spec/suppport/my_helper.rb
module MyHelper
def do_something
# ...
end
end
I'd like to call that helper method like so:
RSpec.configure do |config|
config.include MyHelper
config.before :suite do
do_something
end
end
But when I try that I get an error like undefined local variable or method 'do_something'. I suspect rspec does some kind of lazy/deferred loading and the helper module does not get included immediately.
If I use before :each instead of before :suite, then everything works as expected. Seemingly the module has been included by the time before :each runs, but not by the time before :suite runs.
In my case the block is idempotent so it's not causing any problems as before :each, but it's wastefully inefficient because it really only needs to run once before the suite runs, not before each test. I do use this method in the specs, so I think it's appropriate to keep it in a helper module, but how can I call it in a before :suite block?
I am using rspec-core 3.4.1.

change this ...
Dir[File.expand_path(File.join('..', 'support', '**', '*.rb'), __FILE__)].each { |f| require f }
to this ...
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
and also change this ...
RSpec.configure do |config|
config.include MyHelper
config.before :suite do
do_something
end
end
to be this ...
RSpec.configure do |config|
include MyHelper
config.before :suite do
do_something
end
end

Related

Passing a hash as an argument in Ruby

I am trying to implement a module that contains the data to be used in tests. Here is my module:
authentication.rb
module Helpers
module Authentication
def sign_in_as
admin = {
mobile_number: "123456789",
password: "123456"
}
end
end
end
The module is called in spec_helper file:
spec_helper.rb
RSpec.configure do |config|
config.include Helpers::Authentication
end
The file below is my method for receiving the login credentials:
login_screen.rb
def login_as(**hash)
mobile_number_textfield.send_keys(hash[mobile_number])
password_textfield.send_keys(hash[password])
next_button.click()
end
When I call the function from my module in my spec file, the credentials are not entered:
login_spec.rb
RSpec.describe('Login') do
before(:all) do
puts "something here"
end
it('should login as founder') do
#login_screen.login_as(sign_in_as)
end
end
How could I pass a hash to my login method?
You need to use symbols as hash keys when you access it:
def login_as(**hash)
mobile_number_textfield.send_keys(hash[:mobile_number])
password_textfield.send_keys(hash[:password])
next_button.click()
end
Your code probably raises error in mobile_number_textfield.send_keys(hash[:mobile_number]).
You can do either of the following:
def login_as(mobile_number:, password:)
mobile_number_textfield.send_keys(mobile_number)
password_textfield.send_keys(password)
next_button.click()
end
def login_as(hash)
mobile_number_textfield.send_keys(hash[:mobile_number])
password_textfield.send_keys(hash[:password])
next_button.click()
end
login_as({mobile_number: "02980298098", password: "password"})
My solution to works:
In my module I created a function only with a hash:
authentication.rb
module Helpers
module Authentication
def sign_in_as
{
mobile_number: '123456789',
password: '123456'
}
end
end
end
My spec_helper remains the same
spec_helper.rb
require_relative './helpers/authentication'
RSpec.configure do |config|
config.include Helpers::Authentication
end
On my login_screen file, to each line that I want to send a hash value I added a symbol:
login_screen.rb
def login_as(**hash)
mobile_number_textfield.send_keys(hash[:mobile_number])
password_textfield.send_keys(hash[:password])
next_button.click()
end
In my login_spec file, I just called sign_in_as function (created in my module)
Tip: In the spec file you don't need to require the module because the added line config.include Helpers::Authentication in the spec_helper file make this.
login_spec.rb
RSpec.describe('Login') do
before(:all) do
puts "something here"
end
it('should login as founder') do
#login_screen.login_as(sign_in_as)
end
end

RSpec: Always execute before(:all) in begin/rescue

I'm writing Selenium tests, using Watir-Webdriver and RSpec, which can be a bit spotty when they're first being developed. I've run into a situation where I want to create something on the UI in before :all, however it can throw exceptions (based on timing or poor loading). When that happens I want to take a screenshot.
Here's what I have:
RSpec.configure do |config|
config.before(:all) do |group| #ExampleGroup
#browser = Watir::Browser.new $BROWSER
begin
yield #Fails on yield, there is no block
rescue StandardError => e
Utilities.create_screenshot(#browser)
raise(e)
end
end
end
I run it and get an error:
LocalJumpError: no block given (yield)
The reason I assumed yielding would work is RSpec's definition of before:
def before(*args, &block)
hooks.register :append, :before, *args, &block
end
How can I wrap the code I've put in my before :all in a begin/rescue block without having to do it on every suite?
Thanks in advanced.
The code you've written in the before hook is the &block you're referring to in RSpec::Hooks#before. The hook yields to your code, then runs your tests after the yield is complete.
As for how to make this work, I think this should do:
RSpec.configure do |config|
# ensures tests are run in order
config.order = 'defined'
# initiates Watir::Browser before all tests
config.before(:all) do
#browser = Watir::Browser.new $BROWSER
end
# executes Utilities.create_screenshot if an exception is raised by RSpec
# and the test is tagged with the :first metadata
config.around(:each) do |example|
example.run
Utilities.create_screenshot(#browser) if example.exception && example.metadata[:first]
end
end
This configuration requires the first test be tagged with metadata:
describe Thing, :first do
it "does something" do
# ...
end
end
This way, you'll only take a screenshot at the beginning of your run for a failing test, and not after every failing test. If you'd rather not mess with metadata (or prefer your tests are run in random order), you could do something like this:
RSpec.configure do |config|
# initiates Watir::Browser before all tests
config.before(:all) do
#test_count = 0
#browser = Watir::Browser.new $BROWSER
end
# executes Utilities.create_screenshot if an exception is raised by RSpec
# and the test is the first to run
config.around(:each) do |example|
#test_count += 1
example.run
Utilities.create_screenshot(#browser) if example.exception && #test_count == 1
end
end
This works for me. Instead of begin/rescue in the before :all hook,
config.after :each do
example_exceptions = []
RSpec.world.example_groups.each do |example_group|
example_group.examples.each do |example|
example_exceptions << !example.exception.nil?
end
end
has_exceptions = example_exceptions.any? {|exception| exception}
#Handle if anything has exceptions
end

Automatically share context in RSpec

I want to share a memoized method between my specs. So I tried to use shared context like this
RSpec.configure do |spec|
spec.shared_context :specs do
let(:response) { request.execute! }
end
end
describe 'something' do
include_context :specs
end
It works ok. But I have about 60 spec files, so I'm forced to explicitly include context in each of them. Is there an way to automatically include shared context (or at least let definition) for all example groups in spec_helper.rb?
Something like this
RSpec.configure do |spec|
spec.include_context :specs
end
You can set up global before hooks using RSpec.configure via configure-class-methods and Configuration:
RSpec.configure {|c| c.before(:all) { do_stuff }}
let is not supported in RSpec.configure, but you can set up a global let by including it in a SharedContext module and including that module using config.before:
module MyLetDeclarations
extend RSpec::Core::SharedContext
let(:foo) { Foo.new }
end
RSpec.configure { |c| c.include MyLetDeclarations }
In RSpec 3+, this can be achieved as follows - based on Jeremy Peterson's answer.
# spec/supprt/users.rb
module SpecUsers
extend RSpec::SharedContext
let(:admin_user) do
create(:user, email: 'admin#example.org')
end
end
RSpec.configure do |config|
config.include SpecUsers
end
You can do it almost like that: there's a mechanism for including a module, and module inclusion has its own callback mechanism.
Suppose for example that we have a disconnected shared context that we want to use to run all our model specs without a database connection.
shared_context "disconnected" do
before :all do
ActiveRecord::Base.establish_connection(adapter: :nulldb)
end
after :all do
ActiveRecord::Base.establish_connection(:test)
end
end
You can now create a module that will include that context on inclusion.
module Disconnected
def self.included(scope)
scope.include_context "disconnected"
end
end
Finally, you can include that module into all specs in the normal manner (I've demonstrated doing it only for models, just to show that you can), which is almost exactly what you asked for.
RSpec.configure do |config|
config.include Disconnected, type: :model
end
That works with rspec-core 2.13.0 and rspec-rails 2.13.0.
Another way to go is to automatically share examples via metadata. So:
shared_context 'a shared context', a: :b do
let(:foo) { 'bar' }
end
describe 'an example group', a: :b do
# I have access to 'foo' variable
end
The most common way I use it is in rspec-rails, with some shared context depending on the example group type. So if you have config.infer_spec_type_from_file_location!, you can simply do:
shared_context 'a shared context', type: :controller do
let(:foo) { 'bar' }
end
describe SomeController do
# I have access to 'foo' variable
end
Also if you need ability to use shared data in before blocks inside specs, as me, try to include this (if its Rails project):
module SettingsHelper
extend ActiveSupport::Concern
included do
attr_reader :default_headers
before :all do
#default_headers = Hash[
'HTTP_HOST' => 'test.lvh.me'
]
end
after :all do
#default_headers = nil
end
end
end
RSpec.configure do |config|
config.include SettingsHelper
end
Or try something similar, look at #threedaymonk answer.

rspec shared_context and include_context for all specs

I'm trying to define a few let's and before hooks that will run globally for all my specs by including them in a separate file using the Rspec configuration block.
I tried something like:
module Helpers
def self.included(base)
base.let(:x){ "x" }
base.before(:all){ puts "x: #{x}" }
end
end
Rspec.configure{|c| c.include Helpers }
but this doesn't work as expected. The before(:all) doesn't just run before each main example group, but each nested one as well.
Then I found out about shared_context and it appears to be exactly what I want.
My open problem however is that I can't figure out how to share a context amongst ALL of my specs. The docs only reference include_context within a specific spec.
Can anyone tell me how I can achieve this behavior in a global manner? I'm aware that I can define global before hooks in my spec_helper but I can't seem to use let. I'd like a single place that I can define both of these things and not pollute my spec helper, but just include it instead.
I tried to reproduce your error, but failed.
# spec_helper.rb
require 'support/global_helpers'
RSpec.configure do |config|
config.include MyApp::GlobalHelpers
end
# support/global_helpers.rb
module MyApp
module GlobalHelpers
def self.included(base)
base.let(:beer) { :good }
base.before(:all) { #bottles = 10 }
end
end
end
# beer_spec.rb
require 'spec_helper'
describe "Brewery" do
it "makes good stuff" do
beer.should be :good
end
it "makes not too much bottles" do
#bottles.should == 10
end
context "when tasting beer" do
before(:all) do
#bottles -= 1
end
it "still produces good stuff" do
beer.should be :good
end
it "spends some beer on degusting" do
#bottles.should == 9
end
end
end
https://gist.github.com/2283634
When I wrote something like base.before(:all) { p 'global before'; #bottles = 10 }, I got exactly one line in spec output.
Notice that I didn't try to modify instance variables inside an example, because it wouldn't work anyway (well, actually you can modify instance variables, if it's a hash or array). Moreover, even if you change before(:all) in nested example group to before(:each), there will be still 9 bottles in each example.

Webrat Mechanize outside of Rails

I'm trying to use Webrat in a standalone script to automate some web browsing. How do I get the assert_contain method to work?
require 'rubygems'
require 'webrat'
include Webrat::Methods
include Webrat::Matchers
Webrat.configure do |config|
config.mode = :mechanize
end
visit 'http://gmail.com'
assert_contain 'Welcome to Gmail'
I get this error
/usr/lib/ruby/gems/1.8/gems/webrat-0.6.0/lib/webrat/core/matchers/have_content.rb:57:in 'assert_contain': undefined method assert' for #<Object:0xb7e01958> (NoMethodError)
assert_contain and other assertions are methods of test/unit, try to require it and use webrat from inside a test method:
require 'test/unit'
class TC_MyTest < Test::Unit::TestCase
def test_fail
assert(false, 'Assertion was false.')
end
end
anyway i haven't tested it but I have a working spec_helper for rspec if this can interest you:
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
require 'spec/rails'
require "webrat"
Webrat.configure do |config|
config.mode = :rails
end
module Spec::Rails::Example
class IntegrationExampleGroup < ActionController::IntegrationTest
def initialize(defined_description, options={}, &implementation)
defined_description.instance_eval do
def to_s
self
end
end
super(defined_description)
end
Spec::Example::ExampleGroupFactory.register(:integration, self)
end
end
plus a spec:
# remember to require the spec helper
describe "Your Context" do
it "should GET /url" do
visit "/url"
body.should =~ /some text/
end
end
give it a try I found it very useful (more than cucumber and the other vegetables around) when there is no need to Text specs (features) instead of Code specs, that I like the most.
ps you need the rspec gem and it installs the 'spec' command to execute your specs.

Resources