How to mix a module into an rspec context - ruby

How can I mix a module into an rspec context (aka describe), such that the module's constants are available to the spec?
module Foo
FOO = 1
end
describe 'constants in rspec' do
include Foo
p const_get(:FOO) # => 1
p FOO # uninitialized constant FOO (NameError)
end
That const_get can retrieve the constant when the name of the constant cannot is interesting. What's causing rspec's curious behavior?
I am using MRI 1.9.1 and rspec 2.8.0. The symptoms are the same with MRI 1.8.7.

You want extend, not include. This works in Ruby 1.9.3, for instance:
module Foo
X = 123
end
describe "specs with modules extended" do
extend Foo
p X # => 123
end
Alternatively, if you want to reuse an RSpec context across different tests, use shared_context:
shared_context "with an apple" do
let(:apple) { Apple.new }
end
describe FruitBasket do
include_context "with an apple"
it "should be able to hold apples" do
expect { subject.add apple }.to change(subject, :size).by(1)
end
end
If you want to reuse specs across different contexts, use shared_examples and it_behaves_like:
shared_examples "a collection" do
let(:collection) { described_class.new([7, 2, 4]) }
context "initialized with 3 items" do
it "says it has three items" do
collection.size.should eq(3)
end
end
end
describe Array do
it_behaves_like "a collection"
end
describe Set do
it_behaves_like "a collection"
end

You can use RSpec's shared_context:
shared_context 'constants' do
FOO = 1
end
describe Model do
include_context 'constants'
p FOO # => 1
end

Related

Deprecating an old module name in Ruby?

How would I best go about renaming a Ruby Module in a backwards compatible manner? I have ActiveSupport, so can call deprecation warnings this way, but in this case I'm deprecating the old module name and not the method names.
Example follows:
module OldName
def self.method1
...
end
end
Will become:
module NewName
def self.method1
...
end
end
I want to generate warnings directing the developer to using NewName::method1 rather than OldName::method1
Any suggestions?
ActiveSupport provides two ways of deprecating a constant: DeprecatedConstantAccessor and DeprecatedConstantProxy. They each have trade-offs and limitations in when they show the warning (Accessor is when it's accessed; Proxy is when its methods are called) and how they generally behave. You'll probably want to try both to see which one feels right for your use case.
Rails 4.1.8 Ruby 2.2.0p0
module Fred
extend self
def aaa; end
def bbb; end
def ccc; end
def ddd; end
def eee; end
end
module Bar
extend self
def ccc; end
end
ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
Fred.aaa
DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 4.2. (called from __pry__ at (pry):15)
=> nil
Same code tried in rails 5.2.0 but no DEPRECATION WARNING.
Rails 5.2.0 Ruby 2.5.1p57
class Fred
def aaa; end
def bbb; end
def ccc; end
def ddd; end
def eee; end
end
class Bar
def ccc; end
end
ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
> Fred.new.aaa
DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 6.0 (called from irb_binding at (irb):13)
=> nil
You could go with something like:
module Bar
def self.aaa
'Bar::aaa'
end
def baz
'Bar#baz'
end
end
module Foo
{included: :include,extended: :extend,prepended: :prepend}.each do |mod_method, called_method|
singleton_class.define_method(mod_method) do |base|
warn "Foo is deprecated please use Bar instead"
base.send(called_method,Bar)
end
end
def self.method_missing(meth,*args,&block)
warn "Foo is deprecated please use Bar instead"
Bar.respond_to?(meth) ? Bar.send(meth,*args,&block) : super
end
end
class A
include Foo # this will throw a warning
end
Then
Foo.aaa
# Foo is deprecated please use Bar instead
#=> 'Bar::aaa'
A.new.baz
#=> 'Bar#baz'
module OldName
def instance_method(*args)
yield(args)
end
instance_methods.each do |im|
alias_name = "_#{im.to_s}"
alias_method alias_name, im
define_method(im) do |*args, &block|
warn "Foo is deprecated please use Bar instead"
public_send(alias_name, *args, &block)
end
end
end
class C
include OldName
end
C.instance_methods && [:im, :_im]
#=> [:im, :_im]
c = C.new
c._instance_method(2,3) { |a,b| a+b }
#=> 5
c.instance_method(2,3) { |a,b| a+b }
# Foo is deprecated please use Bar instead
#=> 5
Alternatively, public_send(alias_name, *args, &block) can be replaced with method(alias_name).call(*args, &block).
I was sideswiped by a bug in the gem I was patching, which caused a few false starts, but the final answer is extremely simple and I include the working code as an example for others:
require 'active_support'
OldName = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OldName', 'NewName')
module NewName
def self.method1
puts 'Hello World!'
end
end
NewName.method1
#=> Hello World!
OldName.method1
#=> DEPRECATION WARNING: OldName is deprecated! Use NewName instead. (called from ..)
#=> Hello World!
Simples!
My thanks to #matthewd for re-pointing me in the right direction.

Dynamically adding statements to a method

I have a class Foo:
class Foo
def a
"something" if true
end
end
I want an add_statement method to add new statements to the method, keeping the old implementation. Is it possible?
I want to do something like this:
foo = Foo.new
foo.extend_method(:a, &block)
so now the source of my a method should be something like this:
def a
"something" if true
&block
end
where &block is the code I passed as argument in extend_method.
You want to use alias_method_chain, or Module#prepend.
There are many tutorials / documentation about them on the net, for example "AVOIDING ALIAS_METHOD_CHAIN IN RUBY" or "Module.prepend: a super story".
For your specific example (when you want to extend the function outside of the class), you have to use alias_method_chain, added in a new module, and included in your original class using Foo.send.
You can find more details and examples in "When to use alias_method_chain".
Here is an aspect oriented example:
require 'aspector'
class Foo
def a
puts "in Foo.a"
end
end
aspect = Aspector do
before :a do
puts 'this should print first'
end
after :a do |result_of_a|
puts 'this should print last'
end
end
puts "\n\nplain instance"
foo = Foo.new
foo.a
puts "\n\napply to an instance"
aspect.apply(foo)
foo.a
puts "\n\napply to all instances of Foo"
aspect.apply(Foo)
Foo.new.a
You will need to 'gem install aspector'.

Ruby: what's the difference with these self extensions?

So I have three different ways I can have class level methods on a module:
I usually do it this way if I have 3 or fewer:
module Foo
def self.speak
"Foo"
end
end
If I have more, I've traditionally done it this way:
module Bar
class << self
def speak
"Bar"
end
end
end
But recently, I came across this nifty way and have started using this more often:
module FooBar
extend self
def speak
"FooBar"
end
end
They all work:
Foo.speak => "Foo"
Bar.speak => "Bar"
FooBar.speak => "FooBar"
So, is there any material differences or gotchas to be aware of, esp. with the last form? The only real gotcha I can think of is that once you "extend self" all method defs following are class-level.
I've tried to think of some edge cases where one form works, but the other doesn't. Are there any?
The third form, unlike the first two, creates both an instance method :speak and a module method :speak:
module Foo
def self.speak
"Foo"
end
end
Foo.methods.include?(:speak) #=> true
Foo.instance_methods.include?(:speak) #=> false
class A
include FooBar
end
A.instance_methods.include?(:speak) #=> false
module Bar
class << self
def speak
"Bar"
end
end
end
Bar.methods.include?(:speak) #=> true
Bar.instance_methods.include?(:speak) #=> false
class A
include Bar
end
A.instance_methods.include?(:speak) #=> false
module FooBar
extend self
def speak
"FooBar"
end
end
FooBar.methods.include?(:speak) #=> true
FooBar.instance_methods.include?(:speak) #=> true
class A
include FooBar
end
A.instance_methods.include?(:speak) #=> true

How do you temporarily "class_eval" for a test?

Is it possible to temporarily apply certain methods to a class for tests? I want to be able to run specs depending on many ways to apply it. While I could make a bunch of fixtures with different settings, I find it easier to just class_eval the model in the tests. For example:
describe "some context"
before do
Page.class_eval do
my_applying_method :some => :option
end
end
it "should..."
end
Then in another context block:
describe "another context without the gem applied"
before do
Page.class_eval do
# nothing here since I want to page to be as is
end
end
it "should do something else..."
end
But the problem with the last context block is that it has a modified class (modified in the context block above). Is it possible to reset a class after class_eval? How?
Thanks!
I hope there is a better way to do that but you can use this(and it's with a warning at the Foo = Foo_old line):
module Bar
def baz
end
end
class Foo
end
puts Foo.method_defined? :baz #=> false
Foo_old = Foo.dup # create a copy of our class
Foo.class_eval do
include Bar
end
puts Foo.method_defined? :baz #=> true
Foo = Foo_old
puts Foo.method_defined? :baz #=> false
You haven't clarified how you are modifying the class.
The remix library allows you to temporarily include a module and properly uninclude it later.
In general, it's probably safest to just duplicate the class and test the duplicate:
irb(main):001:0> class Foo; end
#=> nil
irb(main):002:0> Foo.dup.class_eval{ #x = 42 }
#=> 42
irb(main):003:0> Foo.class_eval{ #x }
#=> nil

Rewrite shared example groups in rspec2

In rspec 1 I could do
describe "Something", :shared => true do
include SomeModule # which has the :a_method method
def a_method(options)
super(options.merge(:option => #attr)
end
it "foofoofoo" do
end
end
describe "Something else" do
before(:each) do
#attr = :s_else
end
it_should_behave_like "Something"
it "barbarbar" do
a_method(:name => "something else")
Something.find("something else").name.should == "Something else"
end
...
end
That is, I could use :shared => true to not only refactor examples but also share method definitions and attributes. I realize the example is contrived, but how would one write it in rspec >= 2 without touching the SomeModule module or the Something class?
You can do this with shared_examples_for
shared_examples_for "something" do
include SomeModule # which has the :a_method method
def a_method(options)
super(options.merge(:option => #attr))
end
it "foofoofoo" do
end
end
And call with it_behaves_like:
it_behaves_like "something"
EDIT
Joao correctly points out that this fails to include SomeModule for the examples in the describe block. The include would have to take place outside the shared example group, e.g. at the top of the spec file
include SomeModule # which has the :a_method method
# ...
shared_examples_for "something" do
def a_method(options)
super(options.merge(:option => #attr))
end
it "foofoofoo" do
end
end
David Chelimsky discusses some new features of shared examples in RSpec 2 which may be pertinent in this blog post.

Resources