Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Mocking new and other class methods

51 views
Skip to first unread message

jeem

unread,
Mar 14, 2005, 3:12:00 PM3/14/05
to
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 @callCoun...@handlers.length
result
end
def restoreKlassNew
@klass.restoreNew
end
def verify
raise VerifyFailure.new, "Expected
#...@handlers.length}
cal...@handlers.length==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

Robert Klemme

unread,
Mar 14, 2005, 5:15:50 PM3/14/05
to

"jeem" <jeem....@gmail.com> schrieb im Newsbeitrag
news:1110831120.5...@o13g2000cwo.googlegroups.com...

> 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

jeem

unread,
Mar 14, 2005, 6:06:26 PM3/14/05
to
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

unread,
Mar 14, 2005, 6:10:31 PM3/14/05
to
>> Blocks with ensure could help here as transactional context

(I just noticed this part of your note.)

That's what MockNew.use does.

Robert Klemme

unread,
Mar 15, 2005, 8:59:26 AM3/15/05
to

"jeem" <jeem....@gmail.com> schrieb im Newsbeitrag
news:1110841586....@g14g2000cwa.googlegroups.com...

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

unread,
Mar 15, 2005, 9:00:56 AM3/15/05
to

"jeem" <jeem....@gmail.com> schrieb im Newsbeitrag
news:1110841831....@o13g2000cwo.googlegroups.com...

> >> 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


jeem

unread,
Mar 15, 2005, 9:47:12 AM3/15/05
to
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.

Robert Klemme

unread,
Mar 15, 2005, 10:11:57 AM3/15/05
to

"jeem" <jeem....@gmail.com> schrieb im Newsbeitrag
news:1110898032.5...@f14g2000cwb.googlegroups.com...

> 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

jeem

unread,
Mar 15, 2005, 3:12:39 PM3/15/05
to
Hi Robert. This method looks interesting. I'll give it a try.

Jim

0 new messages