Ruby metaprogramming, how does RSpec's 'should' work?
Take a look at class OperatorMatcher.
It all boils down to Ruby allowing you to leave out periods and parenthesis. What you are really writing is:
target.should.send(:==, 5)
That is, send the message should
to the object target
, then send the message ==
to whatever should
returns.
The method should
is monkey patched into Kernel
, so it can be received by any object. The Matcher
returned by should
holds the actual
which in this case is target
.
The Matcher
implements the method ==
which does the comparison with the expected
which, in this case, is the number 5. A cut down example that you can try yourself:
module Kernel
def should
Matcher.new(self)
end
end
class Matcher
def initialize(actual)
@actual = actual
end
def == expected
if @actual == expected
puts "Hurrah!"
else
puts "Booo!"
end
end
end
target = 4
target.should == 5
=> Booo!
target = 5
target.should == 5
=> Hurrah!
Mocking a dynamically-generated class in ruby metaprogramming with rspec
Instead of:
@mock_model = mock('Garb::Model')
@mock_model.stub(:results) # doesn't work!
I think you want to do:
Garb::Model.any_instance.stub(:results)
This will stub out any instance of Garb::Model to return results. You need to do this because you are not actually passing @mock_model into any class/method that will use it so you have to be a bit more general.
Using metaprogramming to write a single spec for multiple workers
You can use shared examples. Assuming all of them have a "operation" class of sorts that will perform a call
, maybe something like this:
shared_examples_for "a sidekiq worker" do |operation_klass|
subject(:worker) { described_class.new }
describe "perform" do
let(:some_id) { instance_double("String") }
it "calls some operation" do
expect(operation_klass).to receive(:call).with(some_id: some_id)
worker.perform(some_id)
end
it "enqueues a worker" do
described_class.perform_async(some_id)
expect(described_class.jobs.size).to eq 1
end
end
end
RSpec.describe HardWorker do
it_behaves_like "a sidekiq worker", HardWorkingOperation
end
If you need to also check that the call
is done with a different set of arguments each worker, you could pass that in as a hash I guess. But at that point, you should be asking yourself, if that spec really should be extracted out at all :P
shared_examples_for "a sidekiq worker" do |operation_klass, ops_args|
..
expect(operation_klass).to receive(:call).with(ops_args)
..
end
it_behaves_like "a sidekiq worker", HardWorkingOperation, { some_id: some_id }
How does target.should be work in rspec
be
is a method which returns an RSpec Matcher, as mentioned by d11wtq in a comment above
You can implement any other method which returns a matcher, but there are other, simpler ways to write a matcher.
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
describe 9 do
it "should be a multiple of 3" do
9.should be_a_multiple_of(3)
end
end
What exactly is the keyword should in RSpec Ruby
Upon loading, RSpec includes a module into the Kernel
module which is included into all objects known to Ruby. Thus, it can make the should method available to all objects. As such, should
is not a keyword (like if
, class
, or end
) but an ordinary method.
Note that that mixin is only available in RSpec contexts as it is "patched in" during loading or RSpec.
Meta Programming and Unit Testing in ruby
In order to test .register
in isolation, you would need a getter or maybe a .registered?
method which would allow you to access the internals of your singleton class.
You could also test .register
by registering and then creating a class:
describe AisisWriter::ClassFactory do
describe '.create' do
let(:klass) { Hash.new }
before { AisisWriter::ClassFactory.register('Foo', ClassStub, []) }
it "registers the class" do
instance = AisisWriter::ClassFactory.create('Foo', [])
expect(instance).to be_a
end
end
end
Apart from that your code is littered with issues - you may want to read up on how class variables work - and what attr_accessor
does when placed in class eval.
Also you could reduce the arity and complexity of register by using:
def register(klass, params = nil)
class_name = klass.name
end
Ruby uses ?
at the end of interrogative methods is_a?
, nil?
.
How do I write an RSpec test to unit-test this interesting metaprogramming code?
One very simple way to "clone" your Config
class is to simply subclass it with an anonymous class:
c = Class.new Configurator::Config
c.attr_option :foo
d = Class.new Configurator::Config
d.attr_option :foo, :bar
This runs for me without error. This works because all instance variables and methods that get set are tied to the anonymous class instead of Configurator::Config
.
The syntax Class.new Foo
creates an anonymous class with Foo
as a superclass.
Also, throw
ing an Exception
in Ruby is incorrect; Exception
s are raise
d. throw
is meant to be used like a goto
, such as to break out of multiple nests. Read this Programming Ruby section for a good explanation on the differences.
As another style nitpick, try not to use if not ...
in Ruby. That's what unless
is for. But unless-else is poor style as well. I'd rewrite the inside of your args.each
block as:
raise "already have attr_option for #{a}" if self.method_defined?(a)
define_method "#{a}" do
@options[:"#{a}"] ||= {}
end
define_method "#{a}=" do |v|
@options[:"#{a}"] = v
end
Related Topics
Ruby Method, Proc, and Block Confusion
How to Completely Wipe Rubygems Along with Rails etc
How to Bundle Install Gemfile with Specific Version of Bundler
What Are Some Examples of Using Nokogiri
How to Run Two Methods in Parallel Ruby
Killing Process Group from Ruby Kills My Whole Computer
Why Does Ruby Use Nil to Name the Null Object
E: Unable to Locate Package Heroku-Toolbelt
Can't Install Therubyracer in Jruby
How to Do Fuzzy Substring Matching in Ruby
Should Rbenv Be Installed System-Wide, or at a User Level
Anything Speaking Against the Bitnami.Org Ruby/Rails/Redmine Stack
Bundle Install Not Running from My Post-Update Hook
Converting External CSS to Inline CSS for Mail in Rails
How to Pass Named Arguments to a Rake Task