[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