Mocking a method with a block

Discussion in 'Ruby' started by Fernando Guillen, Jul 31, 2010.

  1. Hi people,

    I have an application that download emails from an email account an
    process them.

    What I would like to do is to mock the mail download petition and not
    process the real mails but an array of mails I have for this propos.

    This is the precise situation: I have this:

    Code:
    Net::POP3.start( opts[:server], port, opts[:user], opts[:pass] )
    do |pop|
    pop.each_mail do |m|
    block.call( m )
    end
    end
    
    I would like to have a mock that if on my test call to

    => Net::pOP3.start( opts[:server], port, opts[:user], opts[:pass] ) do
    |pop|

    Not any mailing petition is done but the body of the method is still
    working but not with real mails but with an array of fake mails like
    this:

    => mails = [ File.read('/dir/mail1.raw_mail'),
    File.read('/dir/mail2.raw_mail')]

    Is this possible?.. am I completely lost?.. is there any better way to
    do this?

    Any suggestion is welcome.

    Thanks

    f.
    --
    Posted via http://www.ruby-forum.com/.
    Fernando Guillen, Jul 31, 2010
    #1
    1. Advertising

  2. On Sat, Jul 31, 2010 at 5:38 PM, Fernando Guillen
    <> wrote:
    > Hi people,
    >
    > I have an application that download emails from an email account an
    > process them.
    >
    > What I would like to do is to mock the mail download petition and not
    > process the real mails but an array of mails I have for this propos.
    >
    > This is the precise situation: I have this:
    >
    >
    Code:
    > =A0 =A0 =A0Net::POP3.start( opts[:server], port, opts[:user], opts[:pass]=[/color]
    )[color=blue]
    > do |pop|
    > =A0 =A0 =A0 =A0pop.each_mail do |m|
    > =A0 =A0 =A0 =A0 =A0block.call( m )
    > =A0 =A0 =A0 =A0end
    > =A0 =A0 =A0end
    > 
    >
    > I would like to have a mock that if on my test call to
    >
    > =3D> Net::pOP3.start( opts[:server], port, opts[:user], opts[:pass] ) do
    > |pop|
    >
    > Not any mailing petition is done but the body of the method is still
    > working but not with real mails but with an array of fake mails like
    > this:
    >
    > =3D> mails =3D [ File.read('/dir/mail1.raw_mail'),
    > File.read('/dir/mail2.raw_mail')]
    >
    > Is this possible?.. am I completely lost?.. is there any better way to
    > do this?


    One option would be to change the start method of Net::pOP3 to do
    exactly what you describe.
    Remember that Ruby is open and you can redefine any method on any
    class. You can always alias the method start before changing it, so
    you can revert to the original definition after the test.

    Jesus.
    Jesús Gabriel y Galán, Aug 1, 2010
    #2
    1. Advertising

  3. Jesús Gabriel y Galán wrote:
    > One option would be to change the start method of Net::pOP3 to do
    > exactly what you describe


    This is a good idea.

    I can open the class Net::pOP3 and redefine the method .start.

    Now, how can I organize this to charge the redefinition of the method at
    the beginning of the test and revert it at the end of the test.

    I mean, if I have a file that redefine the Net::pOP3.start method I can
    'require' it but I don't know how to 'un-require' it.

    Also I would like to simulate that the Net::pOP3.start charges my mails
    array (different on every test) and if I redefine this method in a
    generic way I don't know how to use my mails array inside the redefined
    method.

    Example, if I have my own redefinition of the method like:

    Code:
    class Net::POP3
    def self.start( *, &block )
    
    (How can I put my mails array in here?)
    
    end
    end
    
    I think I need some kind of mocking tutorial or something, if you know
    any one and you can offer me the link it will be great.

    Thanks

    f.
    --
    Posted via http://www.ruby-forum.com/.
    Fernando Guillen, Aug 1, 2010
    #3
  4. On Sun, Aug 1, 2010 at 10:40 AM, Fernando Guillen
    <> wrote:
    > Jes=FAs Gabriel y Gal=E1n wrote:
    >> One option would be to change the start method of Net::pOP3 to do
    >> exactly what you describe

    >
    > This is a good idea.
    >
    > I can open the class Net::pOP3 and redefine the method .start.
    >
    > Now, how can I organize this to charge the redefinition of the method at
    > the beginning of the test and revert it at the end of the test.


    You can use alias_method to "save" a copy of the original method with
    another name. You can do this in the setup of your tests, and revert
    back to that version in the teardown:

    irb(main):001:0> class A
    irb(main):002:1> def self.test
    irb(main):003:2> "original test"
    irb(main):004:2> end
    irb(main):005:1> end

    irb(main):013:0> class A
    irb(main):014:1> class << self
    irb(main):015:2> alias_method :eek:rig_test, :test
    irb(main):016:2> end
    irb(main):017:1> end

    # at this point you can call A.test and A.orig_test and both do the same
    # now you can create a new version of the method:

    irb(main):020:0> class A
    irb(main):021:1> def self.test
    irb(main):022:2> "new test, original was [#{orig_test}]"
    irb(main):023:2> end
    irb(main):024:1> end
    =3D> nil
    irb(main):025:0> A.test
    =3D> "new test, original was [original test]"
    irb(main):026:0> A.orig_test
    =3D> "original test"

    As you can see, you can still refer to the original method. To put it back:

    irb(main):027:0> class A
    irb(main):028:1> class << self
    irb(main):029:2> alias_method :test, :eek:rig_test
    irb(main):030:2> end
    irb(main):031:1> end
    =3D> #<Class:A>
    irb(main):032:0> A.test
    =3D> "original test"

    (this leaves the orig_test method around, but I don't think that would
    be a problem).

    >
    > I mean, if I have a file that redefine the Net::pOP3.start method I can
    > 'require' it but I don't know how to 'un-require' it.
    >
    > Also I would like to simulate that the Net::pOP3.start charges my mails
    > array (different on every test) and if I redefine this method in a
    > generic way I don't know how to use my mails array inside the redefined
    > method.


    If this is just for running a test you could use a global variable or
    a constant:

    irb(main):034:0> MY_EMAILS =3D %w{a b c d e}
    =3D> ["a", "b", "c", "d", "e"]
    irb(main):035:0> class A
    irb(main):036:1> class << self
    irb(main):037:2> alias_method :eek:rig_test, :test
    irb(main):038:2> def test
    irb(main):039:3> "my emails: #{MY_EMAILS}"
    irb(main):040:3> end
    irb(main):041:2> end
    irb(main):042:1> end
    =3D> nil
    irb(main):043:0> A.test
    =3D> "my emails: abcde"

    You can also look at class_eval and define_method to build a closure
    around your emails' variable (sorry, don't have time now to do that,
    I'll come back later if you need it).

    Jesus.
    Jesús Gabriel y Galán, Aug 2, 2010
    #4
  5. Jesús, Thank you so much for your help.

    I think I have enough information to work by my self.

    Thanks for your work.

    f.
    --
    Posted via http://www.ruby-forum.com/.
    Fernando Guillen, Aug 2, 2010
    #5
  6. Fernando Guillen wrote:
    > Is this possible?.. am I completely lost?.. is there any better way to
    > do this?


    I'd suggest you use an existing mocking library like "mocha". Example:

    -------- 8< ----------------
    require 'net/pop'
    require 'test/unit'
    require 'rubygems'
    require 'mocha'

    # code to test
    class Foo
    attr_reader :eek:pts
    def initialize(opts)
    @opts = opts
    end
    def bar(block)
    Net::pOP3.start( opts[:server], opts[:port], opts[:user],
    opts[:pass] ) do |pop|
    pop.each_mail do |m|
    block.call( m )
    end
    end
    end
    end

    # tests
    class MyTest < Test::Unit::TestCase
    def test_1
    mails = ["xxxxx","yyy"]
    mockpop = mock
    mockpop.expects:)each_mail).multiple_yields(*mails)
    Net::pOP3.expects:)start).with("127.0.0.1", 110, "a",
    "b").yields(mockpop)

    foo = Foo.new:)server=>"127.0.0.1", :port=>110, :user=>"a",
    :pass=>"b")
    res = []
    foo.bar(lambda { |x| res << x.size })
    assert_equal [5,3], res
    end

    # more expectation-based style
    def test_2
    mails = ["xxxxx","yyy"]
    mockpop = mock
    mockpop.expects:)each_mail).multiple_yields(*mails)
    Net::pOP3.expects:)start).with("127.0.0.1", 110, "a",
    "b").yields(mockpop)

    mockblock = mock
    seq = sequence:)block)
    mockblock.expects:)call).with(mails[0]).in_sequence(seq)
    mockblock.expects:)call).with(mails[1]).in_sequence(seq)

    foo = Foo.new:)server=>"127.0.0.1", :port=>110, :user=>"a",
    :pass=>"b")
    foo.bar(mockblock)
    end
    end
    -------- 8< ----------------

    I find there's something unsatisfying about tests which look like this.
    Sometimes you can spend more effort on the mechanics of mocking than on
    solving the original problem, and the resulting tests are closely
    coupled to the internal implementation of your function. But that *is*
    what you asked for :)

    Refactoring might make your code easier to test. e.g.

    -------- 8< ----------------
    require 'net/pop'
    require 'test/unit'
    require 'rubygems'
    require 'mocha'

    class Foo
    attr_reader :eek:pts
    def initialize(opts)
    @opts = opts
    end
    def bar(block)
    for_all_messages do |m|
    block.call(m)
    end
    end
    private
    def for_all_messages(&blk)
    Net::pOP3.start( opts[:server], opts[:port], opts[:user],
    opts[:pass] ) do |pop|
    pop.each_mail(&blk)
    end
    end
    end

    class MyTest < Test::Unit::TestCase
    def test_1
    foo = Foo.new({})

    mails = ["xxxxx","yyy"]
    foo.expects:)for_all_messages).multiple_yields(*mails)
    res = []
    foo.bar(lambda { |x| res << x.size })
    assert_equal [5,3], res
    end
    end
    -------- 8< ----------------

    HTH,

    Brian.
    --
    Posted via http://www.ruby-forum.com/.
    Brian Candler, Aug 2, 2010
    #6
  7. Brian Candler wrote:
    > Fernando Guillen wrote:
    >> Is this possible?.. am I completely lost?.. is there any better way to
    >> do this?

    >
    > I'd suggest you use an existing mocking library like "mocha".



    This was how I started to try mock the Net::pOP3.

    But instead of using the .yields method, that I didn't know about, I
    tried to mock directly the Net::pOP3.start method and this didn't work.

    Thanks a lot for your comprehensive examples and also for the
    re-factoring suggestion for an easier mock.

    Best regards.

    f.
    --
    Posted via http://www.ruby-forum.com/.
    Fernando Guillen, Aug 2, 2010
    #7
  8. Fernando Guillen wrote:
    > But instead of using the .yields method, that I didn't know about, I
    > tried to mock directly the Net::pOP3.start method and this didn't work.


    It would be really useful if you could attach arbitrary behaviour to a
    mocked method - perhaps 'returns' with a block:

    m = [1,2,3]
    Net::pOP3.expects:)start).returns { m.each { puts m }; m.size }

    This is something I've missed badly in the past, and I've ended up
    mocking directly using Object.new and defining singleton methods.
    --
    Posted via http://www.ruby-forum.com/.
    Brian Candler, Aug 2, 2010
    #8
  9. Hi again people,

    Following your suggestions I am trying to build an small library to
    abstract this behavior on a reusable way:

    * http://github.com/fguillen/NetPopMock

    My problem now is that I'm not able to use the _raw_mails_ array sent on
    the **NetPopMock.fake** call

    See the test example:

    *
    http://github.com/fguillen/NetPopMock/blob/master/test/net_pop_mock_test.rb#L15

    Into the mocked class:

    * http://github.com/fguillen/NetPopMock/blob/master/net_pop_mock.rb#L30

    Jesus was the first one suggesting me to use _class_eval_ and I told him
    that I thought I was enough information to work on my self.. well that
    was not true.. I need help again :)

    I know the solution is in a mix of _eval_ and _binding_ but I don't find
    it.

    Thanks again

    f.
    --
    Posted via http://www.ruby-forum.com/.
    Fernando Guillen, Aug 2, 2010
    #9
  10. Fernando Guillen, Aug 2, 2010
    #10
  11. Fernando Guillen, Aug 2, 2010
    #12
    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. morrell
    Replies:
    1
    Views:
    949
    roy axenov
    Oct 10, 2006
  2. PaoloB

    Mocking OpenOffice in python?

    PaoloB, Mar 14, 2007, in forum: Python
    Replies:
    7
    Views:
    304
    Gerrit Muller
    Mar 23, 2007
  3. gamename

    Unit Testing With Function Mocking

    gamename, Mar 16, 2007, in forum: C Programming
    Replies:
    10
    Views:
    1,069
    gamename
    Mar 19, 2007
  4. mo.sparrow
    Replies:
    0
    Views:
    362
    mo.sparrow
    Aug 22, 2008
  5. Kyung won Cheon
    Replies:
    0
    Views:
    203
    Kyung won Cheon
    Nov 21, 2008
Loading...

Share This Page