Forum: RSpec Mocking base class methods

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.
4f8c518d1177f790894580962678a316?d=identicon&s=25 Tobias Grimm (tobias)
on 2009-02-21 13:01
(Received via mailing list)
Hi!

I need to write some wrapper classes, that derive from SWIG generated
proxy classes for some C/C++ extensions.

The Ruby wrapper classes need to do a lot of 'super' calls to the base
classes generated by SWIG.

The question is: Is there any way to make RSpec mock a base class and
record/verify that certain methods on a base class are called?

As long as the methods in the derived class have different names this is
no problem, but otherwise it doesn't seem to be possible - at least not
with RSpec's mocking/stubbing facilities, does it?

I see other ways to do this, like wrapping an instance of the Swig class
instead of deriving from it or writing some custom mocking code that
fakes
the base class e.g. with define_method, but I'm especially asking if
this
is possible with RSpec.

thx,

Tobias

module Swig
  class Base
    def initialize(arg)
    end

    def do_something(arg)
    end
  end
end

module Wrapper
  class Derived << Swig::Base
    def initialize(arg)
      super(arg)
      yield(self) if block_given?
    end

    def do_something(arg)
      do_something(arg)
      # additional behaviour here
    end
  end
end

describe Wrapper::Derived do
  it 'should initialize the base class with the same argument' do
    # No idea, how to mock Base.initialize() here
  end

  it 'should do something with the base class when doing something' do
    # No idea, how to mock Base.do_something() here
  end
end
F86901feca747abbb5c6c020362ef2e7?d=identicon&s=25 Zach Dennis (zdennis)
on 2009-02-21 21:13
(Received via mailing list)
On Sat, Feb 21, 2009 at 6:31 AM, Tobi <listaccount@e-tobi.net> wrote:
>
> As long as the methods in the derived class have different names this is
> no problem, but otherwise it doesn't seem to be possible - at least not
> with RSpec's mocking/stubbing facilities, does it?
>
> I see other ways to do this, like wrapping an instance of the Swig class
> instead of deriving from it or writing some custom mocking code that fakes
> the base class e.g. with define_method, but I'm especially asking if this
> is possible with RSpec.

+1 to composition over inheritance here. Mocking at different layers
of an inheritance hierarchy sounds like trouble and screams to pull
that thing apart. Composition will allow you to separate an
inheritance hierarchy (if you do need one) from the swig base classes,
and it provides a clean separation between the swig stuff and your
application stuff.

>    def do_something(arg)
>
>  end
>
>  it 'should do something with the base class when doing something' do
>    # No idea, how to mock Base.do_something() here
>  end
> end
> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>



--
Zach Dennis
http://www.continuousthinking.com
http://www.mutuallyhuman.com
4f8c518d1177f790894580962678a316?d=identicon&s=25 Tobias Grimm (tobias)
on 2009-02-21 22:45
(Received via mailing list)
Zach Dennis wrote:

> +1 to composition over inheritance here. Mocking at different layers
> of an inheritance hierarchy sounds like trouble and screams to pull
> that thing apart.

Good point! I've already tried the composition approach. It solves the
testabilitiy problems, but it doesn't feel right in this case.

Inheritance seems to be the more natural approach. The C++ application
will call virtual methods on the base class which should be overriden in
a
derived class. So it seems to make sense to have the same inheritance
tree
in the Ruby counter parts.

If I would use composition, I would at least need to dynamically extend
the referenced base class so I can override some of the methods that are
the counter parts of the virtual C++ methods. And I would need to expose
the referenced class to the C++ code.

I've finally found a way to kinda mock the base class in some way:

module BaseClassMock
  attr_accessor :base

  def self.included(klass)
    class << klass
      @@base_class = Object.new

      def base_class
        return @@base_class
      end
    end
  end

  def initialize(*args)
    @@base_class.new(*args) if @@base_class.respond_to?(:new)
    @base = Object.new
  end

  def method_missing(symbol, *args)
    @base.send(symbol, *args)
  end
end

Any base class (Swig classes in my case) should then be declared for
RSpec
like:

module Swig
  class Base; include BaseClassMock; end
end

And in the specs I can then do:

    Derived.base_class.should_receive(:new).with('something')
    d = Derived.new('something')

or:

    d = Derived.new
    d.base.should_receive(:do_something)
    d.do_something

Tobias
3e626f6013805e96a95e999fc691b21f?d=identicon&s=25 Jeroen van Dijk (Guest)
on 2009-03-19 18:17
(Received via mailing list)
Hi all,

I'm having the same problem as Tobi and I wanted to try Tobi mock
solution
but unfortunately it does not work for me.

Here is a trivial example I want to test:

module ActiveRecord
  class Base
      def self.count_by_params(params = {}, options = {})
          scoped_by_params(params).count
      end
  end
end

And my spec:
module ActiveRecord
  class Base
    include BaseClassMock
  end
end

class DummyUser < ActiveRecord::Base
end

it "should pass all the params and options to #scope_by_params" do
   params  = { :foo => 1, :bar => 2 }
    options = { :foo => 3, :bar => 4 }
    @dummy = DummyUser
    @dummy.base_class.should_receive(:scope_by_params).with(params,
options)
    DummyUser.count_by_params(params, options)
end

With the above example and Tobi's BaseClassMock module I'm getting
"undefined method `should_receive' for #<Object:0x188c164>". Btw, I also
get
this when I do not use Tobi be it for for #<Class:0x188cdf8> instead of
Object.

Any suggestions on what I'm doing wrong or a different approach?

Cheers,
Jeroen
3e626f6013805e96a95e999fc691b21f?d=identicon&s=25 Jeroen van Dijk (Guest)
on 2009-03-20 11:33
(Received via mailing list)
Huray, got it solved! Hopefully you'll agree with this solution (thanks
to
mocha sorry)


  describe "count_by_params" do
    it "should pass all the params and options to #scope_by_params" do
      params  = { :foo => 1, :bar => 2 }
      options = { :foo => 3, :bar => 4 }

      @base = ActiveRecord::Base
      @result = mock("query results", :count => 1)
      @base.expects(:scoped_by_params).with(params,
options).returns(@result)
      @base.count_by_params(params, options)
    end



On Thu, Mar 19, 2009 at 5:35 PM, Jeroen van Dijk
<jeroentjevandijk@gmail.com
F86901feca747abbb5c6c020362ef2e7?d=identicon&s=25 Zach Dennis (zdennis)
on 2009-03-20 15:30
(Received via mailing list)
2009/3/20 Jeroen van Dijk <jeroentjevandijk@gmail.com>:
>       @result = mock("query results", :count => 1)
>       @base.expects(:scoped_by_params).with(params,
> options).returns(@result)
>       @base.count_by_params(params, options)
>     end
>

Is #scoped_by_params a public method that has its own examples? Down
at the model level I would want to know that count_by_params actually
was working as expected and not just collaborating with itself to call
an internal method. I don't know how crazy scoped_by_params is so
maybe there is a potential risk you're willing to accept given the
complexity of what it does, but this example raises my eyebrow as to
the behaviour your actually expecting and verifying.


>>
>>   class Base
>>     @dummy = DummyUser
>> Any suggestions on what I'm doing wrong or a different approach?
>>> > +1 to composition over inheritance here. Mocking at different layers
>>> tree
>>>  attr_accessor :base
>>>
>>> Any base class (Swig classes in my case) should then be declared for
>>>    d = Derived.new('something')
>>> rspec-users@rubyforge.org
>>> http://rubyforge.org/mailman/listinfo/rspec-users
>>
>
>
> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>



--
Zach Dennis
http://www.continuousthinking.com
http://www.mutuallyhuman.com
3e626f6013805e96a95e999fc691b21f?d=identicon&s=25 Jeroen van Dijk (Guest)
on 2009-03-21 19:47
(Received via mailing list)
> >     end
> >
>
> Is #scoped_by_params a public method that has its own examples? Down
> at the model level I would want to know that count_by_params actually
> was working as expected and not just collaborating with itself to call
> an internal method. I don't know how crazy scoped_by_params is so
> maybe there is a potential risk you're willing to accept given the
> complexity of what it does, but this example raises my eyebrow as to
> the behaviour your actually expecting and verifying.
>


scoped_by_params is indeed a public method with its own examples. I
agree
that this spec does not say much, it is not more than a verification of
its
implementation i guess. In sum, I have three methods find_by_params,
count_by_params and scoped_by_params. The first two rely on
scoped_by_params, which in turn relies on dynamic scopes from
ActiveRecord::Base.

I see two possible ways of testing this; an integration test in which we
focus on the result of the queries. Another possibility, the one I use
in
this example, is to check if other methods are called correctly.

My reasoning is that ActiveRecord::Base has an extensive test suite, so
I
only need to test that it is called correctly. By mocking methods call
I'm
having simpler and faster tests. The problem is that the test by itself
will
not tell me when Rails internals are changed and my code has actually
been
broken. I'm assuming that the end result should be tested by integration
tests, do you agree?

What would your test strategy be?

(The code is actually part of a small plugin I'm writing and can be
found
here
http://github.com/jeroenvandijk/find_by_params/blo...
)


Jeroen

PS I'm a TDD beginner so feel free correct me :)
This topic is locked and can not be replied to.