Forum: Ruby Unit testing with mock objects

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
2d3ec3a83b4f8784d6853564fa0d2e77?d=identicon&s=25 Dido Sevilla (Guest)
on 2006-05-11 23:59
(Received via mailing list)
I've been writing an extensive set of unit tests for a bunch of code
that I've been developing over the past couple of years to improve its
maintainability and have in the process needed to make more than a few
mock objects to encapsulate functionality like databases, web
services, distributed objects, and peripherals to which the program
interfaces. I've been wondering what is the best way to refactor these
kinds of classes so that it becomes easy to inject mock objects where
these domain objects are required. What I've been doing so far is to
attach a block to the initialize method that, when specified, is
called to instantiate these mock objects, e.g.:

class Foo
  def initialize(x, y, &block)
    if block_given?
      dobjs = block.call
      @domain_obj1 = dobjs[:domain_obj1]
      @domain_obj2 = dobjs[:domain_obj2]
    else
      @domain_obj1 = DomainObj1.new
      @domain_obj2 = DomainObj2.new
    end
  end

so that in my testing code I can do the following:

FlexMock.use("domain_obj1") do |dobj1|
  FlexMock.use("domain_obj2").do |dobj2|
    f = Foo.new(x, y) { {:domain_obj1 => dobj1, :domain_obj2 => dobj2 }
}
  end
end

but this hardly feels like the "right" way to do it. It feels like
such a kludge. I know I could write factory methods to create my
domain objects and then reopen the class at testing time and rewrite
the factory methods to return the mock objects, but then it's not
exactly clear how I could feed these rewritten factory methods with
mock objects instantiated as above, from within the test. I could
rewrite the initialization method to accept instances of my domain
objects instead so that at test time I could just feed it with my mock
objects, but that would also require me to rewrite all the code that
uses instances of the class in question as well, and that's not such a
good idea to do everywhere. Any suggestions?
Cff9eed5d8099e4c2d34eae663aae87e?d=identicon&s=25 Jacob Fugal (Guest)
on 2006-05-12 17:46
(Received via mailing list)
On 5/11/06, Dido Sevilla <dido.sevilla@gmail.com> wrote:
>   end
>
> so that in my testing code I can do the following:
>
> FlexMock.use("domain_obj1") do |dobj1|
>   FlexMock.use("domain_obj2").do |dobj2|
>     f = Foo.new(x, y) { {:domain_obj1 => dobj1, :domain_obj2 => dobj2 } }
>   end
> end

I like the "trailing option hash" "pattern" (used loosely). It's
pretty close to what you've got, but syntactlcally a little nicer. I'd
do it like this for your demonstrated class:

  class Foo
    DEFAULTS = {
      :domain_obj1 => DomainObj1,
      :domain_obj2 => DomainObj2
    }

    def initialize(x, y, options={})
      options.merge!(DEFAULTS)
      @domain_obj1 = options[:domain_obj1]
      @domain_obj2 = options[:domain_obj2]
      @domain_obj1 = @domain_obj1.new if Class === @domain_obj1
      @domain_obj2 = @domain_obj2.new if Class === @domain_obj2
    end
  end

I use the classes in the DEFAULTS array so that each instance of Foo
can have it's own DomainObj1 (in the default case), rather than having
a reference to the same object that was instantiated when the hash was
created. The "x = x.new if Class === x" "pattern" (again, loosely
using the P word) is also useful in that you can pass in a MockClass
as well, rather than an instantiated object, if you want. Example
usage:

  # passing mock objects
  FlexMock.use("domain_obj1") do |dobj1|
    FlexMock.use("domain_obj2").do |dobj2|
      f = Foo.new(x, y,
        :domain_obj1 => dobj1,
        :domain_obj2 => dobj2)
    end
  end

--OR--

  # passing mock classes
  f = Foo.new(x, y,
    :domain_obj1 => MockDomainObj1,
    :domain_obj2 => MockDomainObj2)

--OR--

  # normal, non-test usage, should look same as before:
  f = Foo.new(x, y)

How's that look?

Jacob Fugal
Cff9eed5d8099e4c2d34eae663aae87e?d=identicon&s=25 Jacob Fugal (Guest)
on 2006-05-12 17:49
(Received via mailing list)
On 5/12/06, Jacob Fugal <lukfugl@gmail.com> wrote:
>     def initialize(x, y, options={})
>       options.merge!(DEFAULTS)
>       @domain_obj1 = options[:domain_obj1]
>       @domain_obj2 = options[:domain_obj2]
>       @domain_obj1 = @domain_obj1.new if Class === @domain_obj1
>       @domain_obj2 = @domain_obj2.new if Class === @domain_obj2
>     end
>   end

Shame on me for posting without testing... I got my merge backwards,
it should be:

  options = DEFAULTS.merge(options)

Since the values in the argument, not receiver, take precedence if
both have a key.

Jacob Fugal
This topic is locked and can not be replied to.