Sunday, July 26, 2009

Mocks and Stubs in ROR with Mocha



Mock objects and stubs are very useful strategies in Test Driven Development(TDD).They play a great role in increasing the effectiveness and speed of the unit tests.

Life is easier when we write unit tests for methods that returns in-process immediate results.Like adding two integers and returning the result.Things get complicated when the method interacts with applications in another process and involve in a blocking call (connecting to database,calling external web service methods,sending mail through a mail server).

For a variety of reasons, interacting with external resources can make out automated tests a lot harder to write, debug, and understand.It also takes a significant amount of time to run the tests.To extend unit test coverage and overcome this limitation,an alternative best practice is to use mock objects, stubs, or other fake objects in place of the external resources that initiate an inter process communication.

The terms "Mock" and "Stub" are used interchangeably in most cases and thought as same.But there are subtle difference in the usage of these two.Martin Fowler explains the difference in this renowned article.

In my understanding so far,Mocks and Stubs are similar in nature but Mocks does more in the form of "Interaction based testing".

Stub is a class where the method definitions imitates original methods of a class that is involved in calling external resources.The imitated methods returns hard coded known result.So the stub methods return expected outputs against a set of known inputs without involving call to external resources as the original class methods do.

For example,if the original "execute_DML" method involves initiating a connection to database,executing the SQL query against it and returning a true/false, the stub "execute_DML" method just returns true or false depending on the input.

Mocks does the same as Stubs, but it considers the interaction between classes.We use mock objects to record and verify the interaction.In the above example the mock "execute_DML" method not only returns true/false but also verifies that the method calls the appropriate methods to initiate the connection and sending the SQL query to database.In this case, the connection and query execution methods are also mocked to avoid external resource calls.

In mock methods, if we set an expectation to call a method of a particular object only once and in reality the method is called twice, the test result is considered a failure even if the final outcome of the method is satisfied.This is different from stubs.

In ScrumPad project, we use mocks and stubs in the above mentioned scenarios.We use an excellent framework Mocha for this.

Mocha is a library for mocking and stubbing that provides a unified, simple and readable syntax.Let me provide some examples:


We can mock a "class" method.suppose we have a method "add_to_cart".The original definition is the following,

def add_to_cart(product_id)
 product = Product.find(product_id)

 if (product != nil)
   Cart.add(product)
 end

 return true
end


The call to "Product.find" involves a database interaction.We can use mock methods to avoid this.


require 'test/unit'
require 'mocha'

class MiscExampleTest < Test::Unit::TestCase

 def test_mocking_a_class_method
  product = Product.new
  Product.expects(:find).with(1).returns(product)
  assert_equal add_to_cart(1), true
 end
end


In the above case,we have set an expectation in the "add_to_cart" method on "Product" class.Inside the "add_to_cart" method, there must be a call to the "find" method of "Product" class.If the call is omitted,the test will fail even if the final assertion is passed (e.g. the return value of the method "add_to_cart" is true).As we have mocked the "find" class method of the "Product" active record class,there will be no actual database interaction.

We can also set how many times a particular method should be invoked.For example if we set the expectation in the above example like this:

Product.expects(:find).once.with(1).returns(product)

This will raise error if we call the "find" method more than once inside the "add_to_cart" method.

We can set expectation on "instance" methods in stead of "class" methods.For example

Product.any_instance.expects(:find).with(1).returns(product)


Using Stubs instead of mocks is very similar in mocha.For example:


def test_stubbing_an_instance_method_on_all_instances_of_a_class
 recepient_token = "124399A_@@44"
 PaymentService.any_instance.stubs(:create_recipient_token).
 returns(recepient_token)

 transaction_response = TransactionResponse.new()
 transaction_response.stubs(:status).
 returns(SUCCESS)

 PaymentEngine.handle_payments()
end


In the above example, we are using stubs to return predefined results from the imitated methods.

Inside the "handle_payments" method,calls to "create_recipient_token" method of "PaymentService" web service will not invoke the web service in reality but will just return the hard-coded string.

Similarly calls to the "status" method of "TransactionResponse" class will always return "Success".

In both cases we are not setting any interaction based expectations as we did with mocks (the "expects" keyword in mocha).We are just freezing the external resource invocation by returning the known result with "Stubs".

The simplicity of "mocha" and usage of these two extremely powerful tool helping us a lot in increasing the test code coverage of ScrumPad.

2 comments:

Sohan said...

Arif,
I liked the post on mock and interaction testing. While setting expectation with arguments, is it possible to specify an expression?
For example, the method should expect any number in between 0 to 100 as an argument instead of just a specific number, say 10. I have found situations where this type of conditional expectation setting becomes important.

Arif said...

Interesting question.Mocha framework supports expressions and variable parameters in many ways.For Example:

If a matching_block is given, the block is called with the parameters passed to the expected method. The expectation is matched if the block evaluates to true.

object = mock()
object.expects(:expected_method).with() { |value| value % 4 == 0 }
object.expected_method(16)
# => verify succeeds

object = mock()
object.expects(:expected_method).with() { |value| value % 4 == 0 }
object.expected_method(17)
# => verify fails



It also enables to specify a range of expectation for method calls.For example:


object = mock()
object.expects(:expected_method).times(2..4)
3.times { object.expected_method }
# => verify succeeds

object = mock()
object.expects(:expected_method).times(2..4)
object.expected_method
# => verify fails


You can get more details on this by going through the API documentation of "Mocha::Expectation" class from here.