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