Ruby Metaprogramming, How Does Rspec's 'Should' Work

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, throwing an Exception in Ruby is incorrect; Exceptions are raised. 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



Leave a reply



Submit