Understanding Avdi's objects on rails code - ruby-on-rails

I'm reading through Avdi's objects on rails book and don't understand a section of sample code.
He creates a class like so I guess for dependency injection purposes:
class Blog
# ...
attr_writer :post_source
# ...
private
def post_source
#post_source ||= Post.public_method(:new)
end
end
Then he writes the following spec
# spec/models/blog_spec.rb
require 'ostruct'
describe Blog do
# ...
describe "#new_post" do
before do
#new_post = OpenStruct.new
#it.post_source = ->{ #new_post }
end
it "returns a new post" do
#it.new_post.must_equal #new_post
end
it "sets the post's blog reference to itself" do
#it.new_post.blog.must_equal(#it)
end
end
end
I don't understand why he uses #it.post_source = ->{ #new_post }
Why didn't he just use something like #it.post_source = OpenStruct.public_method(:new) which would be similar to his Blog class code which has #post_source ||= Post.public_method(:new)
Is there a reason for this?

->{ #new_post } is a lambda that returns the instance stored in #new_post.
Post.public_method(:new) would return the constructor method of Post
Passing in the lambda for the class to use lets you have control of the instance that is returned. Passing in a class' constructor means you don't know what instance it will get, just that it will be of the class you specified.

It provides a reference for the it spec, otherwise it couldn't be compared in the it "returns a new post" block. It uses the simple dependency injection mechanism allowed in the post_source method to ease testability.

Related

Ruby/Rails testing - access variable outside of scope?

I want to unit test a method with rspec for RoR and have a method like this:
def create_record(obj, params)
begin
obj.add_attributes(params)
result = obj.save
rescue
MyMailer.failed_upload(#other_var, obj.api_class_name, params).deliver_now
end
end
create_record is never invoked directly, but through another method which fills in #other_var appropriately.
How should I go about testing the code to make sure MyMailer is called correctly? Should I have passed #other_var into the method instead of relying on it being filled in elsewhere (aka: is this a code smell?)? Thanks!
In Ruby you can use Object#instance_variable_set to set any instance variable.
RSpec.describe Thing do
describe "#create_record" do
let(:thing) do
t = Thing.new
t.instance_variable_set(:#other_var, "foo")
t
end
# ...
end
end
This completely circumvents any encapsulation which means that the use of instance_variable_set can be considered a code smell.
Another alternative is to use RSpecs mocking and stubbing facilities but stubbing the actual object under test is also a code smell.
You can avoid this by passing the dependency as a parameter or by constructor injection:
class Thing
attr_accessor :other_var
def initialize(other_var: nil)
#other_var = other_var
end
def create_record(obj, attributes)
# ...
end
end
A good pattern for this is service objects.

Dynamic generation of tests for an ActiveSupport::Concern

I have a Concern defined like this:
module Shared::Injectable
extend ActiveSupport::Concern
module ClassMethods
def injectable_attributes(attributes)
attributes.each do |atr|
define_method "injected_#{atr}" do
...
end
end
end
end
and a variety of models that use the concern like this:
Class MyThing < ActiveRecord::Base
include Shared::Injectable
...
injectable_attributes [:attr1, :attr2, :attr3, ...]
...
end
This works as intended, and generates a set of new methods that I can call on an instance of the class:
my_thing_instance.injected_attr1
my_thing_instance.injected_attr2
my_thing_instance.injected_attr3
My issue comes when I am trying to test the concern. I want to avoid manually creating the tests for every model that uses the concern, since the generated functions all do the same thing. Instead, I thought I could use rspec's shared_example_for and write the tests once, and then just run the tests in the necessary models using rspec's it_should_behave_like. This works nicely, but I am having issues accessing the parameters that I have passed in to the injectable_attributes function.
Currently, I am doing it like this within the shared spec:
shared_examples_for "injectable" do |item|
...
describe "some tests" do
attrs = item.methods.select{|m| m.to_s.include?("injected") and m.to_s.include?("published")}
attrs.each do |a|
it "should do something with #{a}" do
...
end
end
end
end
This works, but is obviously a horrible way to do this. Is there an easy way to access only the values passed in to the injectable_attributes function, either through an instance of the class or through the class itself, rather than looking at the methods already defined on the class instance?
Since you say that you "want to avoid manually creating the tests for every model that uses the concern, since the generated functions all do the same thing", how about a spec that tests the module in isolation?
module Shared
module Injectable
extend ActiveSupport::Concern
module ClassMethods
def injectable_attributes(attributes)
attributes.each do |atr|
define_method "injected_#{atr}" do
# method content
end
end
end
end
end
end
RSpec.describe Shared::Injectable do
let(:injectable) do
Class.new do
include Shared::Injectable
injectable_attributes [:foo, :bar]
end.new
end
it 'creates an injected_* method for each injectable attribute' do
expect(injectable).to respond_to(:injected_foo)
expect(injectable).to respond_to(:injected_bar)
end
end
Then, as an option, if you wanted to write a general spec to test whether an object actually has injectable attributes or not without repeating what you've got in the module spec, you could add something like the following to your MyThing spec file:
RSpec.describe MyThing do
let(:my_thing) { MyThing.new }
it 'has injectable attributes' do
expect(my_thing).to be_kind_of(Shared::Injectable)
end
end
What about trying something like this:
class MyModel < ActiveRecord::Base
MODEL_ATTRIBUTES = [:attr1, :attr2, :attr3, ...]
end
it_behaves_like "injectable" do
let(:model_attributes) { MyModel::MODEL_ATTRIBUTES }
end
shared_examples "injectable" do
it "should validate all model attributes" do
model_attributes.each do |attr|
expect(subject.send("injected_#{attr}".to_sym)).to eq (SOMETHING IT SHOULD EQUAL)
end
end
end
It doesn't create individual test cases for each attribute, but they should all have an assertion for each attribute. This might at least give you something to work from.

Meta Programming and Unit Testing in ruby

I am using rspec to do my testing and while I am not looking for full blown answer, more of a pseudo code approach to get me going - I am unsure how to write tests for the following class.
module AisisWriter
class ClassFactory
class << self
undef_method :new
attr_accessor :registered_objects
def register(class_name, klass, params = nil)
if !params.is_a(Array)
raise ArgumentError, "params must be an array"
end
registered_object[class_name] = {:class_name => klass, :params => !params.nil? ? params.flatten : nil}
end
def create(class_name, params = nil)
if !params.is_a(Array)
raise ArgumentError, "params must be an array"
end
klass = registered_object[class_name]
if !params.nil
klass[:class_name].new(params.flatten)
else
flass[:class_name].new(*klass[:params])
end
end
end
end
end
I write this class in rails to allow me to register frequently used classes and be able to quickly create them on the fly. This concept comes from my question on reflection in ruby.
What I am unsure of how to test is two things. One I have to make sure the class you register is actually registered. Two that if you try and create an instance of the class that its actually created.
I could write a get_registered_objects function to help with the first one and compare it to an existing object or traverse it looking for specific attributes.
But the second part, how do you test that a class was instantiated?
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?.

Define class in Rspec

I want to test an inclusion of a module into a class. I am trying define a new class in RSpec:
describe Statusable do
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
end
describe '#statuses' do
it 'sets STATUSES for a model' do
test_class::STATUSES.should == ["опубликовано", "черновик"]
end
end
end
And I get an error:
TypeError:
[ActiveModel::Validations::InclusionValidator] is not a class/module
This is probably because in Statusable I have:
validates_inclusion_of :status, :in => statuses,
:message => "{{value}} должен быть одним из: #{statuses.join ','}"
But if I comment it out, I get:
TypeError:
["опубликовано", "черновик"] is not a class/module
Maybe new class definition isn't the best option, what do I do then? And even if it's not, how can I define a class in RSpec? And how do I fix this error?
Do not define new constant in tests otherwise it will pollute other tests. Instead, use stub_const.
Also, for this is an unit test of Statusable module. If ActiveRecord model is not a necessity, better not to use it.
You can also use class_eval to avoid not opening this class(no matter fake or not) actually
describe Statusable do
before do
stub_const 'Foo', Class.new
Foo.class_eval{ include Statusable }
Foo.class_eval{ statuses published: "foo", draft: "bar"}
end
context '#statuses' do
it 'sets STATUSES for a model' do
FOO::STATUSES.should == ["foo", "bar"]
end
end
end
Though I copied your assertion, I would suggest not to insert a constant say STATUS into the class/module(Foo) who includes this module. Instead, a class method would be better
expect(Foo.status).to eq(["foo", "bar"])
It fails because class definition does not return itself.
$ irb
> class Foo; 1 end
=> 1
you need to do like this:
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
ModelIncludingStatusable # return the class
end
It works but unfortunately, ModelIncludingStatusable will be defined on top-level because of ruby rule.
To capsulize your class, you should do like this:
class self::ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
let(:test_class) do
self.class::ModelIncludingStatusable # return the class
end
It works perfectly :)
When you call let this define a memoized helper method. You can't class definition in method body.
Another option which I frequently use is to put the entire test in it's own module, e.g.
module Mapping::ModelSpec
module Human
Person = Struct.new(:name, :age, :posessions)
Possession = Struct.new(:name, :value)
end
RSpec.describe Mapping::Model do
it 'can map with base class' do
person = Human::Person.new('Bob Jones', 200, [])
...
end
end
end
While this is a bit cumbersome, it avoids polluting the global namespace, is only slightly more syntax, and is generally easy to understand. Personally, I'd like a better option.. but I'm not sure what that would be.

Mocking/stubbing a method that's included from "instance.extend(DecoratorModule)"

I use a decorator module that get's included in a model instance (through the "extends" method). So for example :
module Decorator
def foo
end
end
class Model < ActiveRecord::Base
end
class ModelsController < ApplicationController
def bar
#model = Model.find(params[:id])
#model.extend(Decorator)
#model.foo
end
end
Then I would like in the tests to do the following (using Mocha) :
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
get :bar
end
Is this possible somehow, or do you have in mind any other way to get this functionality???
Just an Assumption Note: I will assume that your Decorator foo method returns "bar" which is not shown in the code that you sent. If I do not assume this, then expectations will fail anyway because the method returns nil and not "bar".
Assuming as above, I have tried the whole story as you have it with a bare brand new rails application and I have realized that this cannot be done. This is because the method 'foo' is not attached to class Model when the expects method is called in your test.
I came to this conclusion trying to follow the stack of called methods while in expects. expects calls stubs in Mocha::Central, which calls stubs in Mocha::ClassMethod, which calls *hide_original_method* in Mocha::AnyInstanceMethod. There, *hide_original_method* does not find any method to hide and does nothing. Then Model.foo method is not aliased to the stubbed mocha method, that should be called to implement your mocha expectation, but the actual Model.foo method is called, the one that you dynamically attach to your Model instance inside your controller.
My answer is that it is not possible to do it.
It works (confirmed in a test application with render :text)
I usually include decorators (instead of extending them at runtime) and I avoid any_instance since it's considered bad practice (I mock find instead).
module Decorators
module Test
def foo
"foo"
end
end
end
class MoufesController < ApplicationController
def bar
#moufa = Moufa.first
#moufa.extend(Decorators::Test)
render :text => #moufa.foo
end
end
require 'test_helper'
class MoufesControllerTest < ActionController::TestCase
# Replace this with your real tests.
test "bar" do
m = Moufa.first
Moufa.expects(:find).returns(m)
m.expects(:foo).returns("foobar")
get :bar, {:id => 32}
assert_equal #response.body, "foobar"
end
end
Ok, now I understand. You want to stub out a call to an external service. Interesting that mocha doesn't work with extend this way. Besides what is mentioned above, it seems to be because the stubbed methods are defined on the singleton class, not the module, so don't get mixed in.
Why not something like this?
test "bar" do
Decorator = Module.new{ def foo; 'foo'; end }
get :bar
end
If you'd rather not get the warnings about Decorator already being defined -- which is a hint that there's some coupling going on anyway -- you can inject it:
class ModelsController < ApplicationController
class << self
attr_writer :decorator_class
def decorator_class; #decorator_class ||= Decorator; end
end
def bar
#model = Model.find(params[:id])
#model.extend(self.class.decorator_class)
#model.foo
end
end
which makes the test like:
test "bar" do
dummy = Module.new{ def foo; 'foo'; end }
ModelsController.decorator_class = dummy
get :bar
end
Of course, if you have a more complex situation, with multiple decorators, or decorators defining multiple methods, this may not work for you.
But I think it is better than stubbing the find. You generally don't want to stub your models in an integration test.
One minor change if you want to test the return value of :bar -
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
assert_equal "bar", get(:bar)
end
But if you are just testing that a model instance has the decorator method(s), do you really need to test for that? It seems like you are testing Object#extend in that case.
If you want to test the behavior of #model.foo, you don't need to do that in an integration test - that's the advantage of the decorator, you can then test it in isolation like
x = Object.new.extend(Decorator)
#.... assert something about x.foo ...
Mocking in integration tests is usually a code smell, in my experience.

Resources