Mock Commands, Stub Queries

Posted by James Mead Fri, 03 Aug 2007 10:10:00 GMT

Zach Moazeni has just posted a suggested patch for Mocha over on his blog. My understanding of the patch is that it means expectations are verified even when an assertion error occurs in the test. Here is his example…


  class Car

    def initialize(parts = [])
      @parts = parts
    end

    def start
      started = true
      @parts.each do | part |
        # commenting out for failure
        # started = started && part.start
      end

      started
    end

  end

  class SomeTest < Test::Unit::TestCase

    def test_start
      engine_mock = mock("engine_mock")
      car = Car.new([engine_mock])

      engine_mock.expects(:start).returns(false)
      assert !car.start
    end

  end

I’ve had a friendly & useful conversation with Zach about it, but I’m not convinced this is the right way to go. Using the one assertion per test school of thought, you can achieve the same goal by splitting the test into two so you get a test failure for the expectation and another for the assertion…


  class SomeTest < Test::Unit::TestCase

    def test_should_start_engine
      engine = mock('engine')
      car = Car.new([engine])

      engine.expects(:start)

      car.start
    end

    def test_should_start_if_engine_starts
      engine = stub('engine')
      car = Car.new([engine])

      engine.stubs(:start).returns(false)

      assert !car.start
    end

  end

Something that makes the example less suitable for mocking is that the Car#start method is both a command and a query. If you separate the two, testing with mocks might be easier…


  class Car

    def initialize(parts = [])
      @parts = parts
    end

    def start
      @parts.each { |part| part.start }
    end

    def started?
      @parts.all? { |part| part.started? }
    end

  end

  class SomeOtherTest < Test::Unit::TestCase

    def test_should_start_engine
      engine = mock('engine')
      car = Car.new([engine])

      engine.expects(:start)

      car.start
    end

    def test_should_not_be_started_if_engine_is_started
      engine = stub('engine')
      car = Car.new([engine])

      engine.stubs(:started?).returns(false)

      assert !car.started?
    end

  end
  

I’d be interested to know what other people think…

One thing I do agree with Zach about is that submitting a suggested patch to an open source project is a great way of initiating a constructive conversation.

Tags , , , , , , , ,  | 4 comments

The Difference between Mocks and Stubs

Posted by James Mead Mon, 11 Sep 2006 02:59:00 GMT

Some recent converstations have made me realise that a lot of people are confused by the difference between mocking and stubbing. I’ve realised that I’ve only added to the confusion by calling part of the Mocha library, Stubba.

The difference between mocking and stubbing

Stubbing a method is all about replacing the method with code that returns a specified result (or perhaps raises a specified exception). Mocking a method is all about asserting that a method has been called (perhaps with particular parameters).

If you think about it, it’s difficult (or impossible?) to do mocking without stubbing – you need to return from the mocked method, so that the code under test can complete execution. So mocking libraries tend to implicitly or explicitly allow you to do stubbing.

Mocking and stubbing with Mocha

To stub a method, use the stubs method. To mock a method, use the expects method.

The difference between Mocha and Stubba

Mocha is a traditional mocking library very much in the JMock mould. Stubba is a separate part of Mocha that allows mocking and stubbing of methods on real (non-mock) classes. It works by moving the method of interest to one side, adding a new stubbed version of the method which delegates to a traditional mock object. You can use this mock object to set up stubbed return values or set up expectations of methods to be called. After the test completes the stubbed version of the method is removed and replaced by the original.

I’m thinking of ditching the name Stubba, because this part of the library is not solely concerned with stubbing. Let me know what you think.

Martin Fowler has a must-read article on why Mocks Aren’t Stubs.

Another great reference is JMocks documentation – in particular Yoga for your Unit Tests.

Read more...

Tags , , , , , , , , ,  | 3 comments

Mocha 0.3 released with Rails plugin

Posted by James Mead Thu, 24 Aug 2006 17:59:00 GMT

A new version of the Mocha mocking and stubbing library developed at Reevoo has been released.

There is now a Ruby on Rails plugin which can be installed like this…

  $ script/plugin install svn://rubyforge.org/var/svn/mocha/trunk

Here are the release notes…

  • Rails plugin.
  • Auto-verify for all expectations, including those on concrete classes.
  • Include each expectation verification in the test result assertion count.
  • Filter out noise from assertion backtraces.
  • Point assertion backtrace to line where failing expectation was created.
  • New yields method for expectations.
  • Create stubs which stub all method calls.
  • Mocks now respond_to? expected methods.

Thanks for patches from Chris.

Enjoy!

Posted in  | Tags , , , , , , , , , , ,  | 14 comments

AutoMocha example

Posted by James Mead Sun, 16 Jul 2006 19:01:00 GMT

Again – I’m not really very pleased with this example, but hopefully it makes some sense. It’s important to realise that the test is not running in a normal Rails environment with the standard auto-require. In fact the first time the Comment class is encountered AutoMocha uses const_missing to supply a Mocha::Mock in its place. From that point on – any further references get the same mock object.

  class Article

    attr_reader :id

    def accepted_comments
      Comment.find_all_by_article_id(self.id).select { |comment| comment.accepted? }
    end

  end

  require 'rubygems'
  require 'auto_mocha'
  require 'test/unit'

  class OrderTest < Test::Unit::TestCase

    include Mocha

    # illustrates stubbing of previously undefined class Comment
    def test_should_return_accepted_comments_for_this_article
      unaccepted_comment = Mock.new(:accepted? => false)
      accepted_comment = Mock.new(:accepted? => true)
      comments = [unaccepted_comment, accepted_comment]
      Comment.stubs(:find_all_by_article_id).returns(comments)
      article = Article.new
      assert_equal [accepted_comment], article.accepted_comments
    end

  end

Tags , , , , , , , , , , ,  | no comments

Stubba example

Posted by James Mead Sun, 16 Jul 2006 19:00:00 GMT

I don’t really like this example, but it’s the best I’ve got at the moment. The any_instance bit is particularly contrived, but hopefully you get the idea.

  class Order

    attr_accessor :shipped_on

    def total_cost
      line_items.inject(0) { |total, line_item| total + line_item.price } + shipping_cost
    end

    def total_weight
      line_items.inject(0) { |total, line_item| total + line_item.weight }
    end

    def shipping_cost
      total_weight * 5 + 10
    end

    class << self

      def find_all
        # Database.connection.execute('select * from orders...
      end

      def number_shipped_since(date)
        find_all.select { |order| order.shipped_on > date }.size
      end

      def unshipped_value
        find_all.inject(0) { |total, order| order.shipped_on ? total : total + order.total_cost }
      end

    end

  end

  require 'rubygems'
  require 'stubba'
  require 'test/unit'

  class OrderTest < Test::Unit::TestCase

    # illustrates stubbing instance method
    def test_should_calculate_shipping_cost_based_on_total_weight
      order = Order.new
      order.stubs(:total_weight).returns(10)
      assert_equal 60, order.shipping_cost
    end

    # illustrates stubbing class method
    def test_should_count_number_of_orders_shipped_after_specified_date
      now = Time.now; week_in_secs = 7 * 24 * 60 * 60
      order_1 = Order.new; order_1.shipped_on = now - 1 * week_in_secs
      order_2 = Order.new; order_2.shipped_on = now - 3 * week_in_secs
      Order.stubs(:find_all).returns([order_1, order_2])
      assert_equal 1, Order.number_shipped_since(now - 2 * week_in_secs)
    end

    # illustrates stubbing instance method for all instances of a class
    def test_should_calculate_value_of_unshipped_orders
      Order.stubs(:find_all).returns([Order.new, Order.new, Order.new])
      Order.any_instance.stubs(:shipped_on).returns(nil)
      Order.any_instance.stubs(:total_cost).returns(10)
      assert_equal 30, Order.unshipped_value
    end

  end

Tags , , , , , , , , ,  | no comments

Mocha example

Posted by James Mead Sun, 16 Jul 2006 18:59:00 GMT

I thought I’d put the examples from the RDoc README up here if only for the syntax highlighting.

  class Enterprise

    def initialize(dilithium)
      @dilithium = dilithium
    end

    def go(warp_factor)
      warp_factor.times { @dilithium.nuke(:anti_matter) }
    end

  end

  require 'rubygems'
  require 'mocha'
  require 'test/unit'

  class EnterpriseTest < Test::Unit::TestCase

    include Mocha

    def test_should_boldly_go
      dilithium = Mock.new
      dilithium.expects(:nuke).with(:anti_matter).at_least_once
      enterprise = Enterprise.new(dilithium)
      enterprise.go(2)
      dilithium.verify
    end

  end

Tags , , , , , , , , ,  | no comments