rspec shared_context and include_context for all specs - ruby

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.

Related

Embed RSpec test in a Ruby class

I often build little single-purpose Ruby scripts like this:
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
end
w = Widget.new
puts w.render_data(w.end_data)
__END__
data set to work on.
I'd like to include RSpec tests directly inside the file while I'm working on it. Something like this (which doesn't work but illustrates what I'm trying to do):
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
def self_test
# This doesn't work but shows what I'm trying to
# accomplish. The goal is to have RSpec run these type
# of test when self_test is called.
describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
end
end
w = Widget.new
w.self_test
__END__
data set to work on.
I understand this is not the normal way to work with RSpec and isn't appropriate in most cases. That said, there are times when it would be nice. So, I'd like to know, is it possible?
There are two things. First off rspec by default won't pollute the global namespace with methods like describe and so on. The second thing is that you need to tell rspec to run the specs after they've been declared.
If you change your self_test method to be
RSpec.describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
RSpec::Core::Runner.invoke
(having of course done require 'rspec' then that will run your specs).
The invoke methods exits the process after running the specs. If you don't want to do that, or need more control over where output goes etc. you might want to drop down to the run method which allows you to control these things.

Unit testing with and without requiring ActiveSupport

I've extracted a single class from a Rails app into a gem. It's very, very simple, but of course I'd like to fully test it (I'm using rspec).
The class does some simple date-calculation. It's not dependent on Rails, but since it started out in a Rails app, and is still used there, it uses ActiveSupport's time zone-aware methods when it can. But, if ActiveSupport isn't available, it should use the std-lib Date methods.
Specifically, it only does this in one single place: Defaulting an optional argument to "today's date":
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
Question is: How do I properly test this? If I require ActiveSupport in my spec_helper.rb, it'll obviously always use that. If I don't require it anywhere, it'll never use it. And if I require it for a single example group, rspec's random execution order makes the testing unpredictable, as I don't know when AS will be required.
I can require maybe it in a before(:all) in a nested group, as nested groups are (I believe) processed highest to deepest. But that seems terribly inelegant.
I could also split the specs into two files, and run them separately, but again, that seems unnecessary.
I could also disable rspec's random ordering, but that's sort of going against the grain. I'd rather have it as randomized as possible.
Any ideas?
Another solution is to mock the current and today methods, and use those for testing. Eg:
# you won't need these two lines, just there to make script work standalone
require 'rspec'
require 'rspec/mocks/standalone'
def test_method(arg = nil)
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
arg
end
describe "test_method" do
let(:test_date) { Date.new(2001, 2, 3) }
it "returns arg unchanged if not nil" do
test_method(34).should == 34
end
context "without Date.current available" do
before(:all) do
Date.stub(:today) { test_date }
end
it "returns Date.today when arg isn't present" do
test_method.should == test_date
end
end
context "with Date.current available" do
before(:all) do
Date.stub(:current) { test_date }
end
it "returns Date.current when arg isn't present" do
test_method.should == test_date
end
end
end
Running with rspec test.rb results in the tests passing.
Also, the stubs are present only in each context, so it doesn't matter what order the specs are run in.
This is more than a little perverse, but it should work. Include ActiveSupport, and then:
context "without ActiveSupport's Date.current" do
before(:each) do
class Date
class << self
alias_method :current_backup, :current
undef_method :current
end
end
end
# your test
after(:each) do
class Date
class << self
alias_method :current, :current_backup
end
end
end
end
I can't really recommend this; I would prefer to split out this one spec and run it separately as you suggested.

Passing an object as subject to rspec

I am running rspec tests on a catalog object from within a Ruby app, using Rspec::Core::Runner::run:
File.open('/tmp/catalog', 'w') do |out|
YAML.dump(catalog, out)
end
...
unless RSpec::Core::Runner::run(spec_dirs, $stderr, out) == 0
raise Puppet::Error, "Unit tests failed:\n#{out.string}"
end
(The full code can be found at https://github.com/camptocamp/puppet-spec/blob/master/lib/puppet/indirector/catalog/rest_spec.rb)
In order to pass the object I want to test, I dump it as YAML to a file (currently /tmp/catalog) and load it as subject in my tests:
describe 'notrun' do
subject { YAML.load_file('/tmp/catalog') }
it { should contain_package('ppet') }
end
Is there a way I could pass the catalog object as subject to my tests without dumping it to a file?
I am not very clear as to what exactly you are trying to achieve but from my understanding I feel that using a before(:each) hook might be of use to you. You can define variables in this block that are available to all the stories in that scope.
Here is an example:
require "rspec/expectations"
class Thing
def widgets
#widgets ||= []
end
end
describe Thing do
before(:each) do
#thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
# #thing is available here
#thing.should have(0).widgets
end
it "can get accept new widgets" do
#thing.widgets << Object.new
end
it "does not share state across examples" do
#thing.should have(0).widgets
end
end
end
You can find more details at:
https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#define-before(:each)-block

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.

What is the best way to define test specs in JSON using a Ruby harness?

I've got an interesting conundrum. I'm in the midst of developing a library to parse PSDs in Ruby. Also, a buddy is simultaneously working on a library to parse PSDs in JavaScript. We would like to share the same unit tests via a git submodule.
We've decided to use a simple JSON DSL to define each test. A single test might look like:
{
"_name": "Layer should render out",
"_file": "test/fixtures/layer_out.psd",
"_exports_to": "test/controls/layer_out_control.png"
}
So, now it's up to us to build the appropriate test harnesses to translate the JSON into the appropriate native unit tests. I've been using MiniTest to get myself up to speed, but I'm running into a few walls.
Here's what I've got so far. The test harness is named TargetPractice for the time being:
# run_target_practice.rb
require 'target_practice'
TargetPractice.new(:test) do |test|
test.pattern = "test/**/*.json"
end
and
# psd_test.rb
class PSDTest < MiniTest::Unit::TestCase
attr_accessor :data
def tests_against_data
# do some assertions
end
end
and
# target_practice.rb
class TargetPractice
attr_accessor :libs, :pattern
def initialize(sym)
#libs = []
#pattern = ""
yield self
run_tests
end
def run_tests
FileList[#pattern].to_a.each do |file|
test_data = JSON.parse(File.open(file).read)
test = PSDTest.new(test_data["_name"]) do |t|
t.data = test_data
end
end
end
end
Unfortunately, I'm having trouble getting a yield in the initialize to stick in my PSDTest class. Also, it appears that a test will run immediately on initialization.
I would like to dynamically create a few MiniTest::Unit::TestCase objects, set their appropriate data properties and then run the tests. Any pointers are appreciated!
I think you are overcomplicating things a bit here. What you need is a parameterized test, which is pretty trivial to implement using mintest/spec:
describe "PSD converter" do
def self.tests(pattern = 'test/**/*.json')
FileList[pattern].map{|file| JSON.parse(File.read(file))}
end
tests.each do |test|
it "satisfies test: " + test["_name"] do
# some assertions using test["_file"] and test["_exports_to"]
end
end
end

Resources