Hi all,
I’m fairly new to Ruby but really enjoying it. I’ve been working on a
medium large project that is non rails but uses ActiveRecord and
assorted other pieces of the rails framework. I’d been finding myself
having more and more difficulty writing unit tests as I went because
many of my classes were calling class methods of other classes
internally–such as the models from rails–and these were hard to factor
out. For example:
class ImportantModel < ActiveRecord::Base
end
class Processor
def self.do_hard_processing(something)
#…
end
end
class SomeClass
def a_method
#...
something = ImportantModel.find_by_a_lot_of_stuff(one, two)
processed = Processor.do_hard_processing(something)
if _the_time_is_right?(processed)
#...etc
end
end
If those had been objects I could have swapped mocks in for them when I
wanted to test SomeClass, but since they’re classes–constants by
definition–I couldn’t! I know about fixtures to help with this
problem, but sometimes (as in the case of the Processor class) they
aren’t really possible, and sometimes you really don’t want to involve
the working of a model…you just want to test SomeClass’s logic with
some hard coded mock objects.
One solution is pretty simple:
class SomeClass
attr_accessor :important_model
attr_accessor :processor
def initialize
@important_model = ImportantModel
@processor = Processor
end
def a_method
something =
important_model.find_by_a_lot_of_stuff(one, two)
processed = processor.do_hard_processing(something)
if _the_time_is_right?(processed)
#...etc
end
end
This is an improvement, because now in a testing environment, for
instance, you could set
sc = SomeClass.new
sc.important_model = HardCodedModel
#or even
sc.imporant_model = mockObjectOfSomeSort
#since there’s nothing says it has to be a class
But there were two things I didn’t like: one, the repetetive
nature–you’re writing some variation of important_model 3 times, for
each class you want to make variable Second of all, I wanted to make it
easy to decide on the class/object returned conditionally or
dynamically, with a block, which can save you a lot of time in testing
code (and could be useful in production, for that matter).
The solution I came up with was to add a class method called
variable_class to Object, which would provide an indirect way to refer
to classes like ImportantModel or Processor (or even allow them to be
not classes at all but arbitrary objects), and which had support for a
block.
require ‘rubygems’
require ‘active_support/inflector’
class Object
def self.variable_class(klass, accessor_name = nil, &block)
klass_str = klass.name
klass_sym = klass_str.to_sym
klass_translated_str =
“#{Inflector.underscore(klass_str.tr_s(’::’,’_’))}_class”
instance_var_sym = “@#{klass_translated_str}”.to_sym
get_method_sym = accessor_name ? accessor_name.to_sym :
klass_translated_str.to_sym
set_method_sym = "#{get_method_sym.to_s}=".to_sym
if block_given?
define_method(get_method_sym) do
block.call(self)
end
else
define_method(get_method_sym) do
instance_variable_get(instance_var_sym) ||
instance_variable_set(instance_var_sym,
klass)
end
define_method(set_method_sym) do |custom_class|
instance_variable_set(instance_var_sym, custom_class)
end
end
end
Basically, given two classes (here I’m just going to make them classes
and not note the object possibility anymore)
class One
def self.do
puts “one”
end
end
class Two
def self.do
puts “two”
end
end
All the following will work:
class SimplestCase
variable_class One
def test
one_class.do
end
end
sc = SimplestCase.new
sc.test #“one”
sc.one_class = Two
sc.test #“two”
The default is to make the class variable settable/gettable with the
underscored name of the class plus _class. Any :: operators get turned
into underscores as well, so Mod::KlassName would become
mod_klass_name_class by default.
If you need a shorter/different name, you can pass it as a symbol in the
second argument slot.
class AlmostAsSimple
variable_class One, :my_own_name
def test
my_own_name.do
end
end
sc = AlmostAsSimple.new
sc.test #“one”
sc.my_own_name = Two
sc.test #“two”
If you want to use a block, you can do it like so:
class BlockExample
@@use_two = false
def self.go_two
@@use_two = true
end
variable_class One do
@@use_two ? Two : One
end
def test
one_class.do
end
end
be = BlockExample.new
be.test #“one”
BlockExample.go_two
be.test #“two”
And if you want to use instance rather than class methods / variables in
the block, self is passed along for you.
class BlockExample2
def initialize
@use_two = false
end
def go_two
@use_two = true
end
def use_two?
@use_two
end
variable_class One do |be2obj|
be2obj.use_two? ? Two : One
end
def test
one_class.do
end
end
be2 = BlockExample2.new
be2.test #“one”
be2.go_two
be2.test #“two”
Using this approach, I’ve been able to actually test the UNITS in my
unit tests, by being able to swap in mocks for classes as well as
objects and isolating the behavior of the class I’m testing. But I
wanted to run it by the people on this fine list to see what suggestions
people had for it before I put it in too many spots. Is there a much
simpler way to accomplish the same? Or something that could be added
now to make it more flexible? Or a way to tighten up the code? I want
it to be as Ruby-like as possible.
Thanks for any critiques or suggestions.
Edmund