Automatically share context in RSpec - ruby

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.

Related

Why can't RSpec find the Airbrake env keys in a test involving Sidekiq when I specify environment?

Here is my setup:
airbrake.rb
require 'airbrake'
Airbrake.configure do |c|
c.ignore_environments = [:test, :development]
c.project_id = ENV['PROJECT_ID']
c.project_key = ENV['PROJECT_KEY']
end
use Airbrake::Rack::Middleware
spec_helper.rb
RSpec.configure do |config|
config.before(:suite) do
FactoryGirl.reload
FactoryGirl.define do
to_create { |instance| instance.save }
end
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
Airbrake.configure(:test) do |c|
c.project_id = ENV['PROJECT_ID']
c.project_key = ENV['PROJECT_KEY']
end
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
config.include FactoryGirl::Syntax::Methods
end
worker_test_spec.rb
require 'spec_helper'
RSpec.describe NotificationWorker do
it "perform should call Airbrake#notify" do
anotification_worker = LNotificationWorker.new
airbrake_notification_worker.perform("some error message"))
expect(Airbrake).to receive(:notify).with("some error message")
end
end
I call Airbrake#notify in other (non-Sidekiq) tests, and they find the appropriate ENV variables just fine.
Yet if I run the above Sidekiq test with the above setup, I get the following error:
Airbrake::Error:
the 'default' notifier isn't configured
But if I change the Airbrake config in spec_helper.rb to:
Airbrake.configure do |c|
c.project_id = ENV['PROJECT_ID']
c.project_key = ENV['PROJECT_KEY']
end
the ENV keys are able to be found in the tests. Why is this?
When you say Airbrake.configure(:test), it does not mean "configure Airbrake for the test RAILS_ENV". Rather :test creates a non-default named notifier. Then you can send specific notifications to that notifier by saying Airbrake.notify("oops", {time: Time.now}, :test). But that is not about development/test/production, it is about categorizing your notifications.
So the problem is that you have configured a notifier named test, but you have not yet configured one named default, and default is what Airbrake wants to use when you don't tell it otherwise. That's why your spec passes when you say simply Airbrake.configure { ... }.

Use helper methods in rspec before :suite block

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

How do I use an rpec shared_examples across different files?

I want to reuse this shared_examples block across different spec files. I want to extract it into a separate file, and pass in the object so it's not always user. Both things I tried failed, is it possible?
describe User do
before { #user = build_stubbed(:user) }
subject { #user }
shared_examples 'a required value' do |key| # trivial example, I know
it "can't be nil" do
#user.send("#{key}=", nil)
#user.should_not be_valid
end
end
describe 'name'
it_behaves_like 'a required value', :name
end
end
Just require the other file. shared_examples work at the top level, so once defined they are always available; so be careful of naming conflicts.
A lot of RSpec users will put the shared example in spec/support/shared_examples/FILENAME.rb. Then in spec/spec_helper.rb have:
Dir["./spec/support/**/*.rb"].sort.each {|f| require f}
Or on Rails projects
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
That is listed in the 'CONVENTIONS' section of the shared example docs.

Unable to use ActiveModel::MassAssignmentSecurity

I am trying to use some functionality in ActiveModel but I'm having trouble making everything work. I've included my class file and the test I'm running.
The test is failing with:
': undefined method `attr_accessible
I really don't know why, since MassAssignmentSecurity will bring that in and it is in fact running. I've also tried to include all of ActiveModel as well but that's doesn't work either. It doesn't seem to matter if I use include or extend to bring in the MassAssignmentSecurity.
If I pass in some attributes in my test to exercise "assign_attributes" in the initialize, that fails as well. I'm fairly new to rails, so I'm hoping I'm just missing something really simple.
TIA.
Using rails 3.2.12
my_class.rb
class MyClass
include ActiveModel::MassAssignmentSecurity
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveSupport::Callbacks
attr_accessible :persisted, :creds
def initialize(attributes = nil, options = {})
#persisted = false
assign_attributes(attributes, options) if attributes
yield self if block_given?
end
end
my_class_spec.rb
require 'spec_helper'
describe MyClass do
before do
#testcase = MyClass.new
end
subject { #testcase }
it_should_behave_like "ActiveModel"
it { MyClass.should include(ActiveModel::MassAssignmentSecurity) }
it { should respond_to(:persisted) }
end
support/active_model.rb
shared_examples_for "ActiveModel" do
include ActiveModel::Lint::Tests
# to_s is to support ruby-1.9
ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
example m.gsub('_',' ') do
send m
end
end
def model
subject
end
end
Yikes! What a mess I was yesterday. Might as well answer my own question since I figured out my issues.
attr_accessible in MassAssignmentSecurity does not work like it does with ActiveRecord. It does not create getters and setters. You still have to use attr_accessor if you those created.
assign_attributes is a connivence function that someone wrote to wrap around mass_assignment_sanitizer and isn't something baked into in MassAssignment Security. An example implementation is below:
def assign_attributes(values, options = {})
sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
send("#{k}=", v)
end
end

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.

Resources