RSpec With Namespaced Dummy Classes + Eigenclass Fun
29 Mar 2016Generally, you don't need to use dummy classes with rspec.
To clarify
Use doubles if you can!
The rspec mocks and stubs work well and they offer some protection against mocks/stubs becoming out of sync with the objects that they are imitating.
But some times you need a dummy class (or feel like you do anyway)
How to RSpec & Namespaced Dummy classes
First things first, I don't want
class Dummy
end
nor do I want
class SomeAnnoying::Namespace::ForAnExample::Dummy
end
or
class SomeAnnoying::Namespace::ForAnotherExample::Dummy
end
Option 1 sucks.
Option 2 is better, if done once only
Option 3 is the same as 2, for n example groups you may need up to n namespaces that need naming, consideration, etc.
This is annoying_
A yummy way
In spec/support/helpers/dummy_class_helpers.rb
module DummyClassHelpers
def dummy_class(name, &block)
let(name.to_s.underscore) do
klass = Class.new(&block)
self.class.const_set name.to_s.classify, klass
end
end
end
In spec/spec_helper.rb
# skip this if you want to manually require
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}
RSpec.configure do |config|
config.extend DummyClassHelpers
end
And finally in your specs you can do
RSpec.shared_examples "JsonSerializerConcern" do
dummy_class(:dummy)
dummy_class(:dummy_serializer) do
def self.represent(object)
end
end
describe "#serialize_collection" do
it "wraps a record in a serializer" do
expect(dummy_serializer).to receive(:represent).with(an_instance_of(dummy)).exactly(3).times
subject.serialize_collection [dummy.new, dummy.new, dummy.new]
end
end
end
Now you have:
> dummy
=> RSpec::ExampleGroups::ApplicationController::BehavesLikeJsonSerializerConcern::SerializeCollection::Dummy
> dummy_serializer
=> RSpec::ExampleGroups::ApplicationController::BehavesLikeJsonSerializerConcern::SerializeCollection::DummySerializer
Nice automatic, no?
Eigenclass fun
As an aside, sometimes you can do crazy stuff like:
dummy_class :dummy do
def errors
@errors ||= Object.new.tap do |o|
def o.full_messages
[]
end
end
end
end
Explanation
eigenclasses are also known as singletons
The instances of the dummy class above have a method errors such that instance_of_dummy.errors
returns an instance of Object. This instance of object has the method full_errors defined on it, such that
> instance_of_object = instance_of_dummy.errors
> instance_of_object.full_messages
=> []
But if we do
> Object.new.full_messages
=> NoMethodError: undefined method `full_messages' for #<Object:0x0055fd025fee98>
What we have is an object who's behaviour is defined by it's class as well as it's eigenclass. The full_messages method exists on the eigenclass for this instance, no others.
It's useful because every object in ruby has an eigenclass
Anything defined on or scoped to an eigenclass is only shared by inheritance, which is a non-issue for the instances of classes unless you're doing serious voodoo.
It also means that you can quickly mock up a specific behaviour without having to define classes, structs or doubles. Not that there's anything wrong with that. But this works too :)