Mocking new and other class methods

Discussion in 'Ruby' started by jeem, Mar 14, 2005.

  1. jeem

    jeem Guest

    Hello group. I wanted to be able to do something like this:

    def testItResizesTheImage
    imageNew=ClassMethodMocker.new(Magick::Image,:new)
    image=FlexMock.new
    imageNew.handle { |imagePath| assert_equal(@imagePath,imagePage);
    image }
    image.mock_handle:)somecall) { |etc| etc }
    # etc
    test_subj.doThatThing
    image.mock_verify
    imageNew.verify
    end

    So, I wrote a few loc to enable it (borrowing from FlexMock) for 'new'
    calls at least. I'm having second thoughts about how I implemented it,
    so I was hoping some in the ng might want to kick it around.

    My big concern with the current method (see below) is that the real new
    might not be "put back" if a coder doesn't use MockNew.use.

    I have other ways in mind to allow mocking of class methods, but they
    all seem to put up new hoops for coders to jump through, and make the
    resulting code less natural.

    Here's the code, excluding the tests:

    class Class
    public :alias_method
    end

    class Object
    def backupNew
    class << self
    alias_method :mocknew_backedup_new, :new
    end
    end
    @@newHandler={}
    def delegateNew(newHandler)
    module_eval { @@newHandler[self]=newHandler; def
    self.new(*args);
    @@newHandler[self].handleNew(*args); end }
    end
    def restoreNew
    class << self
    alias_method :new, :mocknew_backedup_new
    end
    end
    end

    class MockNew
    def initialize(klass,&block)
    klass.backupNew
    klass.delegateNew(self)
    @klass=klass
    @handlers=[]
    @callCount=0
    handle(&block) if block_given?
    end
    def handleNew(*args)
    result=@handlers[@callCount].call(*args)
    @callCount += 1
    restoreKlassNew if @
    result
    end
    def restoreKlassNew
    @klass.restoreNew
    end
    def verify
    raise VerifyFailure.new, "Expected
    #}
    ==1?'':'s'} to #{@klass}.new; received
    #{@callCount}." unless @callCount == @handlers.length
    end

    def handle(&block)
    @handlers << block
    end
    def self.use(klass)
    mockNew=new(klass)
    yield mockNew
    mockNew.verify
    ensure
    mockNew.restoreKlassNew
    end
    class VerifyFailure < StandardError
    end
    end
    jeem, Mar 14, 2005
    #1
    1. Advertising

  2. "jeem" <> schrieb im Newsbeitrag
    news:...
    > Hello group. I wanted to be able to do something like this:
    >
    > def testItResizesTheImage
    > imageNew=ClassMethodMocker.new(Magick::Image,:new)
    > image=FlexMock.new
    > imageNew.handle { |imagePath| assert_equal(@imagePath,imagePage);
    > image }
    > image.mock_handle:)somecall) { |etc| etc }
    > # etc
    > test_subj.doThatThing
    > image.mock_verify
    > imageNew.verify
    > end
    >
    > So, I wrote a few loc to enable it (borrowing from FlexMock) for 'new'
    > calls at least. I'm having second thoughts about how I implemented it,
    > so I was hoping some in the ng might want to kick it around.
    >
    > My big concern with the current method (see below) is that the real new
    > might not be "put back" if a coder doesn't use MockNew.use.


    Blocks with ensure could help here as transactional context.

    > I have other ways in mind to allow mocking of class methods, but they
    > all seem to put up new hoops for coders to jump through, and make the
    > resulting code less natural.


    Sorry, I'm not really sure what you're after. What I extracted from your
    code is that you iterate through a set of new methods and use the original
    one in the end. IMHO you can get that with much less effort and less meta
    programming:

    class MockClass
    def initialize(cl, *handlers)
    @cl = cl
    @handlers = handlers
    @idx = 0
    end

    def new(*a)
    h = @handlers[@idx]

    if h
    @idx += 1
    h.call(*a)
    else
    @cl.new(*a)
    end
    end
    end

    cl_fake = MockClass.new(String,
    lambda {|*a| "1"},
    lambda {|*a| "2"} )

    >> cl_fake.new "a"

    => "1"
    >> cl_fake.new "a"

    => "2"
    >> cl_fake.new "a"

    => "a"
    >> cl_fake.new "a"

    => "a"
    >> cl_fake.new "a"

    => "a"

    Kind regards

    robert
    Robert Klemme, Mar 14, 2005
    #2
    1. Advertising

  3. jeem

    jeem Guest

    Sorry, I'll try to clarify.

    When I type this:

    mn=MockNew.new(A)
    mn.handle { block }

    I'm saying, "Next time someone calls A.new(args), execute this block
    instead." This lets me test code like this:

    def methodToTest
    etc
    a = A.new('bob')
    a.etc
    end

    Usually I want to find some other way to test code like this, but if
    A.new or a.etc hits the file system or network, etc., I really want to
    be able to sub in a mock. Often we deal with this by passing in the
    object, thus isolating the object creation code. This still leaves the
    creation code untested, and a good place for bugs to hide.
    jeem, Mar 14, 2005
    #3
  4. jeem

    jeem Guest

    >> Blocks with ensure could help here as transactional context

    (I just noticed this part of your note.)

    That's what MockNew.use does.
    jeem, Mar 14, 2005
    #4
  5. "jeem" <> schrieb im Newsbeitrag
    news:...
    > Sorry, I'll try to clarify.
    >
    > When I type this:
    >
    > mn=MockNew.new(A)
    > mn.handle { block }
    >
    > I'm saying, "Next time someone calls A.new(args), execute this block
    > instead." This lets me test code like this:
    >
    > def methodToTest
    > etc
    > a = A.new('bob')
    > a.etc
    > end
    >
    > Usually I want to find some other way to test code like this, but if
    > A.new or a.etc hits the file system or network, etc., I really want to
    > be able to sub in a mock. Often we deal with this by passing in the
    > object, thus isolating the object creation code. This still leaves the
    > creation code untested, and a good place for bugs to hide.


    Well, my example is easily modified to do that also, just add

    def handle(&b)
    @handlers << b if b
    end

    My main point was, that you don't have to mess with the original class
    instance. You can just use any object as stand in that implements #new.

    Kind regards

    robert
    Robert Klemme, Mar 15, 2005
    #5
  6. "jeem" <> schrieb im Newsbeitrag
    news:...
    > >> Blocks with ensure could help here as transactional context

    >
    > (I just noticed this part of your note.)
    >
    > That's what MockNew.use does.


    You wrote "My big concern with the current method (see below) is that the
    real new
    might not be "put back" if a coder doesn't use MockNew.use." - from that
    I assumed that you did not use ensure. *If* you use ensure then the
    original code will always be put back in place - regardless whether you do
    normal or exceptional exit.

    Kind regards

    robert
    Robert Klemme, Mar 15, 2005
    #6
  7. jeem

    jeem Guest

    Let me try again.

    class CodeIWantToTest
    def foo(bar)
    f = File.new(@userPath+bar)
    #more
    f.close
    end
    end

    class Tests < Test::Unit::TestCase
    def testFoo
    fileNew=MockNew.new(File)
    mockFile=FlexMock.new
    fileNew.handle { |path| assert_equal('/home/usrname/etc/bob',path);
    mockFile }
    #more
    subj.foo('bob')
    fileNew.verify
    end
    end

    I the test above, I want foo to call File.new. If I were to make a
    mock class (cl_fake) in my test, I couldn't expect foo to call it,
    because foo is not aware of cl_fake. Foo knows about File.

    Maybe I'm missing something. Can you write the example above with the
    method from your post?

    J.
    jeem, Mar 15, 2005
    #7
  8. "jeem" <> schrieb im Newsbeitrag
    news:...
    > Let me try again.
    >
    > class CodeIWantToTest
    > def foo(bar)
    > f = File.new(@userPath+bar)
    > #more
    > f.close
    > end
    > end
    >
    > class Tests < Test::Unit::TestCase
    > def testFoo
    > fileNew=MockNew.new(File)
    > mockFile=FlexMock.new
    > fileNew.handle { |path| assert_equal('/home/usrname/etc/bob',path);
    > mockFile }
    > #more
    > subj.foo('bob')
    > fileNew.verify
    > end
    > end
    >
    > I the test above, I want foo to call File.new. If I were to make a
    > mock class (cl_fake) in my test, I couldn't expect foo to call it,
    > because foo is not aware of cl_fake. Foo knows about File.


    Ah, ok. Thx for clarifying.

    > Maybe I'm missing something. Can you write the example above with the
    > method from your post?


    Well, you had to redefine constant File, which might not be such a good
    idea.

    Then what about a transaction based approach? Like

    module Kernel
    private
    def mock(obj, sym, mock)
    cl = class <<obj; self; end
    old = obj.method sym
    cl.class_eval { define_method(sym, &mock) }

    begin
    yield
    ensure
    cl.class_eval { define_method(sym, &old) }
    end
    end
    end

    >> mock File, :new, lambda {|*f| "new! #{f.inspect}"} do

    ?> File.new "foo", "r"
    >> end

    => "new! [\"foo\", \"r\"]"

    You can even nest that:

    >> mock File, :new, lambda {|*f| "new! #{f.inspect}" } do

    ?> p File.new( "foo", "r" )
    >>

    ?> mock File, :new, lambda {|*f| "new 2: #{f.inspect}" } do
    ?> p File.new( "bar", "w" )
    >> end
    >>

    ?> p File.new( "foo", "r" )
    >> end

    "new! [\"foo\", \"r\"]"
    "new 2: [\"bar\", \"w\"]"
    "new! [\"foo\", \"r\"]"
    => nil

    Kind regards

    robert
    Robert Klemme, Mar 15, 2005
    #8
  9. jeem

    jeem Guest

    Hi Robert. This method looks interesting. I'll give it a try.

    Jim
    jeem, Mar 15, 2005
    #9
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. martinig
    Replies:
    0
    Views:
    348
    martinig
    Jun 27, 2007
  2. martinig
    Replies:
    0
    Views:
    263
    martinig
    Jun 27, 2007
  3. mo.sparrow
    Replies:
    0
    Views:
    351
    mo.sparrow
    Aug 22, 2008
  4. James Mead
    Replies:
    0
    Views:
    96
    James Mead
    Jul 19, 2006
  5. Kenneth McDonald
    Replies:
    5
    Views:
    289
    Kenneth McDonald
    Sep 26, 2008
Loading...

Share This Page