[mocha-developer] A quick guide to Mocha

James Mead jamesmead44 at gmail.com
Sat Sep 9 06:07:39 EDT 2006


Hi Anselm,

Thanks - that's really helpful - I'll take a look :-)

James.

On 08/09/06, Anselm Heaton <anselm at netuxo.co.uk> wrote:
>
> Hello :)
>
> Having just gone through the (admittedly simple) API of Mocha to learn
> how it works, I wrote a quick guide (190 lines) to help others get up to
> speed. I've attached it as RDOC and as HTML.
>
> It is a first draft - please help me improve by giving feedback !
>
> Contents :
> Overview - Quick overview of Mocha
> Unit testing, mock object and stubs - Very quick introduction to mock
> objects and stubs
> Example with Mocha - The core of the guide ; shows how to use Mocha with
> a continuing example
> The Mocha library - How the library is structured
> Mocha tips - Some tips I would've found useful at first :)
>
> Best,
> Anselm
>
> --
> ------------------------------
> Netuxo Ltd
> a workers' co-operative
> providing low-cost IT solutions
> for peace, environmental and social justice groups
> and the radical NGO sector
>
> Registered as a company in England and Wales. No 4798478
> Registered office: 5 Caledonian Road, London N1 9DY, Britain
> ------------------------------
> email office at netuxo.co.uk
> http://www.netuxo.co.uk
> ------------------------------
>
>
>
>  = A quick guide to Mocha
>
>  == Overview
>
>  Mocha is a framework for creating mock objects and stubbing methods of
> existing classes.
>
>  - Mocha lets users create mock objects and test expectations on those
> objects
>  - Mocha lets users stub methods on exiting objects (For a single object,
> for any
>   object of a given class, or for singleton objects)
>  - Mocha is integrated with Ruby's
>   {Test::Unit}[
> http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html
> ]
>   such that failed expectations with fail
>   {Test::Unit}[
> http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html
> ]
>   tests, and stubs will be removed at the end of each test.
>
>  == Unit testing, mock objects and stubs
>
>  {Unit testing}[http://en.wikipedia.org/wiki/Unit_test] is a technique by
> which
>  you write tests for your code to make sure it works as expected, and
> future
>  additions do not break existing code. It is called "unit" testing because
> you
>  only test a single unit of your own code (for instance just one class),
> and you
>  only test your own code. The problem is that code often has many
> dependencies,
>  and it is difficult to test it in isolation.
>
>  There are two ways of dealing with this problem :
>  - <b>Mock objects</b> : substitute real objects with mock objects, and
> verify that
>   the object is being used as expected.
>  - <b>Stubs</b> : Replace specific methods of existing objects (including
> singleton
>   objects) to return canned (pre-defined) data.
>
>  == Example with Mocha
>
>  === Mock objects
>
>  Say we are writting a class for logging error messages to a file, and we
> want to test
>  it. The class responds to two methods : 'output_to' which sets the IO
> object
>  on which to ouput, and 'log' which takes a message to output :
>
>   class MyLogger
>     def output_to(file)
>       @file = file
>     end
>     def log(msg)
>       @file << msg
>     end
>   end
>
>  The problem is the 'file' object - we don't want to use a real file,
> because it might
>  make the test fail even though our code is correct (for instance if we
> don't have
>  write access to the current directory). So we create a mock object to
> replace the
>  file :
>
>   mockfile = mock('file')
>
>  At this point, we want to make sure that '<<' is called on our file
> object (This is
>  called, rightly, an expectation). This is done this way :
>
>   mockfile.expects(:<<)
>
>  Here the call to 'expects' on the mock object does two things :
>  1. It ensures that the test will succeed only if '<<' is called on
> mockfile
>  2. It returns a Mocha::Expectation object
>
>  The Mocha::Expectation objects can be used to further narrow what is
> expected
>  when '<<' is called. For instance if we want to make sure that the call
> to '<<'
>  gets the string 'some message' we can write :
>
>   mockfile.expects(:<<).with('some message')
>
>  Calls to Mocha::Expectation object are chainable, so the expectation can
> be narrowed
>  further in the same line. For instance, we could add that '<<' must be
> called twice
>  with 'some message' :
>
>   mockfile.expects(:<<).times(2).with('some message')
>
>  Mocha::Expectation also allows to define the behaviour of the mocked
> method - what it returns,
>  yields or raises. For instance in this case, '<<' should really return
> mockfile - or we might
>  get into trouble since calls to '<<' can be chained :
>
>   mockfile.expects(:<<).times(2).with('some message').returns(mockfile)
>
>  We are now ready to write our test :
>
>   def test_output_to_file
>     mockfile = mock('file')
>     mockfile.expects(:<<).times(2).with('some message').returns(mockfile)
>
>     MyLogger.output_to(mockfile)
>     MyLogger.log('some message')
>     MyLogger.log('some message')
>   end
>
>  === Stubs
>
>  Next we want to be able to pass a file name directly to 'output_to', so
> we don't have to bother
>  opening the file ourselves. Our new MyLogger class looks like this :
>
>   class MyLogger
>     def output_to(file)
>       if file.kind_of? String
>         @file = File.open(file, 'a+')
>       else
>         @file = file
>       end
>     end
>     def log(msg)
>       @file << msg
>     end
>   end
>
>  And we want to test this new behaviour. Now we encounter a new problem :
> we can't use a mock
>  object to replace File, since we don't have the opportunity to pass that
> object to MyLogger -
>  it uses it directly. So what we will do is replace (or "stub") the call
> to 'File.open' with our
>  own code. To make this as straightforward as possible, Mocha adds methods
> to the 'Object' class,
>  so we can write directly :
>
>   File.expects(:open).with('test.txt', 'a+').returns(mockfile)
>
>  Again, we did two different things :
>  1. We told File that it had to expect a call to 'open' with parameters '
> test.txt' and 'a+'
>    (the test will fail otherwise)
>  2. We replaced 'File.open' shuch that it returns our mock object instead
> of a real IO object.
>
>  Our second test looks like this :
>
>   def test_output_to_named_file
>     # Create our mock IO object
>     mockfile = mock('file')
>     mockfile.expects(:<<).with('some message').returns(mockfile)
>
>     # Stub File.open so that it returns our mock object
>     File.expects(:open).with('test.txt', 'a+').returns(mockfile)
>
>     # Run our code
>     MyLogger.output_to('test.txt')
>     MyLogger.log('some message')
>   end
>
>  == The Mocha library
>
>  The library for mock objects is 'mocha.rb' and the library for stubs is '
> stubba.rb'.
>  These should be loaded after Test::Unit :
>   require 'test/unit'
>   require 'mocha'
>   require 'stubba'
>
>  Mocha provides three methods for creating mock objects, defined in the
> module
>  Mocha::AutoVerify :
>  - *mock* : creates a mock object for which expectations must be
> fullfilled
>  - *stub* : creates a mock object for which expectations do not need to be
> fullfilled
>  - *stub_everything* : creates a mock object that accepts calls to any
> method
>
>  The objects created by those three methods respond to the three methods
> defined in
>  Mocha::MockMethods :
>  - *expects* : Adds an expectation that a given method must be called.
> This returns a
>   Mocha::Expectation object
>  - *stubs* : Adds an expectation that a given method may be called. This
> returns a
>   Mocha::Expectation object
>  - *verify* : Asserts that all expectations have been fulfilled. This is
> called
>   automatically if the mock object was created by one of the methods in
> Mocha::AutoVerify
>
>  See the API documentation of Mocha::Expectation for all the possible
> expectations
>
>  == Mocha tips
>
>  === Code blocks for 'with' and 'returns'
>
>  The 'with' and 'returns' methods accept code blocks, which can be used
> for expecting or
>  returning different results when the same method is called several times.
> For instance
>  the first test in <b>Example with Mocha</b> could test for different
> strings :
>
>   def test_output_to_file
>     mockfile = mock('file')
>     params = ["some message", "another message"]
>     mockfile.expects(:<<).times(2).with { |p| p == params.shift
> }.returns(mockfile)
>
>     MyLogger.output_to(mockfile)
>     MyLogger.log('some message')
>     MyLogger.log('another message')
>   end
>
>  === Shorthand mocks
>
>  Many mock objects really just return the same value for the same method.
> The methods for
>  creating mock objects in Mocha::AutoVerify provide a shortand notation
> for these :
>
>   access = mock({:username => 'joe', :password => 'ragrag'})
>   access.username # Returns 'joe'
>   access.passowrd # Returns 'ragrag'
>
>
>
>
>  A quick guide to Mocha Overview
>
> Mocha is a framework for creating mock objects and stubbing methods of
> existing classes.
>
>    - Mocha lets users create mock objects and test expectations on
>    those objects
>    - Mocha lets users stub methods on exiting objects (For a single
>    object, for any object of a given class, or for singleton objects)
>    - Mocha is integrated with Ruby's Test::Unit<http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html>such that failed expectations with fail
>    Test::Unit<http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html>tests, and stubs will be removed at the end of each test.
>
> Unit testing, mock objects and stubs
>
> Unit testing <http://en.wikipedia.org/wiki/Unit_test> is a technique by
> which you write tests for your code to make sure it works as expected, and
> future additions do not break existing code. It is called "unit" testing
> because you only test a single unit of your own code (for instance just one
> class), and you only test your own code. The problem is that code often has
> many dependencies, and it is difficult to test it in isolation.
>
> There are two ways of dealing with this problem :
>
>    - *Mock objects* : substitute real objects with mock objects, and
>    verify that the object is being used as expected.
>    - *Stubs* : Replace specific methods of existing objects (including
>    singleton objects) to return canned (pre-defined) data.
>
> Example with Mocha Mock objects
>
> Say we are writting a class for logging error messages to a file, and we
> want to test it. The class responds to two methods : 'output_to' which sets
> the IO object on which to ouput, and 'log' which takes a message to output :
>
>
>   class MyLogger
>     def output_to(file)
>       @file = file
>     end
>     def log(msg)
>       @file << msg
>     end
>   end
>
>  The problem is the 'file' object - we don't want to use a real file,
> because it might make the test fail even though our code is correct (for
> instance if we don't have write access to the current directory). So we
> create a mock object to replace the file :
>
>   mockfile = mock('file')
>
>  At this point, we want to make sure that '<<' is called on our file
> object (This is called, rightly, an expectation). This is done this way :
>
>   mockfile.expects(:<<)
>
>  Here the call to 'expects' on the mock object does two things :
>
>    1. It ensures that the test will succeed only if '<<' is called on
>    mockfile
>    2. It returns a Mocha::Expectation object
>
>  The Mocha::Expectation objects can be used to further narrow what is
> expected when '<<' is called. For instance if we want to make sure that the
> call to '<<' gets the string 'some message' we can write :
>
>   mockfile.expects(:<<).with('some message')
>
>  Calls to Mocha::Expectation object are chainable, so the expectation can
> be narrowed further in the same line. For instance, we could add that '<<'
> must be called twice with 'some message' :
>
>   mockfile.expects(:<<).times(2).with('some message')
>
>  Mocha::Expectation also allows to define the behaviour of the mocked
> method - what it returns, yields or raises. For instance in this case, '<<'
> should really return mockfile - or we might get into trouble since calls to
> '<<' can be chained :
>
>   mockfile.expects(:<<).times(2).with('some message').returns(mockfile)
>
>  We are now ready to write our test :
>
>   def test_output_to_file
>     mockfile = mock('file')
>     mockfile.expects(:<<).times(2).with('some message').returns(mockfile)
>
>     MyLogger.output_to(mockfile)
>     MyLogger.log('some message')
>     MyLogger.log('some message')
>   end
>
> Stubs
>
> Next we want to be able to pass a file name directly to 'output_to', so we
> don't have to bother opening the file ourselves. Our new MyLogger class
> looks like this :
>
>   class MyLogger
>     def output_to(file)
>       if file.kind_of? String
>         @file = File.open(file, 'a+')
>       else
>         @file = file
>       end
>     end
>     def log(msg)
>       @file << msg
>     end
>   end
>
>  And we want to test this new behaviour. Now we encounter a new problem :
> we can't use a mock object to replace File, since we don't have the
> opportunity to pass that object to MyLogger - it uses it directly. So what
> we will do is replace (or "stub") the call to 'File.open' with our own code.
> To make this as straightforward as possible, Mocha adds methods to the
> 'Object' class, so we can write directly :
>
>   File.expects(:open).with('test.txt', 'a+').returns(mockfile)
>
>  Again, we did two different things :
>
>    1. We told File that it had to expect a call to 'open' with
>    parameters 'test.txt' and 'a+' (the test will fail otherwise)
>    2. We replaced 'File.open' shuch that it returns our mock object
>    instead of a real IO object.
>
>  Our second test looks like this :
>
>   def test_output_to_named_file
>     # Create our mock IO object
>     mockfile = mock('file')
>     mockfile.expects(:<<).with('some message').returns(mockfile)
>
>     # Stub File.open so that it returns our mock object
>     File.expects(:open).with('test.txt', 'a+').returns(mockfile)
>
>     # Run our code
>     MyLogger.output_to('test.txt')
>     MyLogger.log('some message')
>   end
>
> The Mocha library
>
> The library for mock objects is 'mocha.rb' and the library for stubs is
> 'stubba.rb'. These should be loaded after Test::Unit :
>
>   require 'test/unit'
>   require 'mocha'
>   require 'stubba'
>
>  Mocha provides three methods for creating mock objects, defined in the
> module Mocha::AutoVerify :
>
>    - *mock* : creates a mock object for which expectations must be
>    fullfilled
>    - *stub* : creates a mock object for which expectations do not need
>    to be fullfilled
>    - *stub_everything* : creates a mock object that accepts calls to
>    any method
>
>  The objects created by those three methods respond to the three methods
> defined in Mocha::MockMethods :
>
>    - *expects* : Adds an expectation that a given method must be
>    called. This returns a Mocha::Expectation object
>    - *stubs* : Adds an expectation that a given method may be called.
>    This returns a Mocha::Expectation object
>    - *verify* : Asserts that all expectations have been fulfilled. This
>    is called automatically if the mock object was created by one of the methods
>    in Mocha::AutoVerify
>
>  See the API documentation of Mocha::Expectation for all the possible
> expectations
> Mocha tips Code blocks for 'with' and 'returns'
>
> The 'with' and 'returns' methods accept code blocks, which can be used for
> expecting or returning different results when the same method is called
> several times. For instance the first test in *Example with Mocha* could
> test for different strings :
>
>   def test_output_to_file
>     mockfile = mock('file')
>     params = ["some message", "another message"]
>     mockfile.expects(:<<).times(2).with { |p| p == params.shift}.returns(mockfile)
>
>     MyLogger.output_to(mockfile)
>     MyLogger.log('some message')
>     MyLogger.log('another message')
>   end
>
> Shorthand mocks
>
> Many mock objects really just return the same value for the same method.
> The methods for creating mock objects in Mocha::AutoVerify provide a
> shortand notation for these :
>
>   access = mock({:username => 'joe', :password => 'ragrag'})
>   access.username # Returns 'joe'
>   access.passowrd # Returns 'ragrag'
>
>
> _______________________________________________
> mocha-developer mailing list
> mocha-developer at rubyforge.org
> http://rubyforge.org/mailman/listinfo/mocha-developer
>
>


-- 
James.
http://blog.floehopper.org
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://rubyforge.org/pipermail/mocha-developer/attachments/20060909/6bc0bf22/attachment-0001.html 


More information about the mocha-developer mailing list