Mocha Ruby Library

repository·main·Indexed 22 days ago

https://github.com/freerange/mocha

A Ruby library for mocking and stubbing objects in unit tests. Mocha provides a unified syntax for full and partial mocking and integrates with testing frameworks including Minitest, Test::Unit, RSpec, and Cucumber. It allows developers to define expectations using `expects` and stubs using `stubs`, with support for mocking class methods, instance methods, and any instance of a class via `any_instance`.

Tokens
6.6K
Snippets
19
Records
31
Agent score
79%

What's inside Mocha

  1. How Partial Mocking works

    main

    Partial Mocking allows you to apply stubs or expectations to existing, real objects or classes. This is useful when you want to keep most of an object's original behavior but override specific methods.

    Key patterns:

    • Stubbing an instance method: object.stubs(:method).returns(value)
    • Stubbing a class method: Class.stubs(:method).returns(value)
    • Stubbing all instances of a class: Class.any_instance.stubs(:method).returns(value)
    require 'test/unit'
    require 'mocha/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 - 2 * 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
  2. How Mock Objects work

    main

    A Mock Object is a test double that you create explicitly. You define expectations on it using expects, which specifies which methods should be called, with what arguments, and what they should return. Mocha automatically verifies these expectations at the end of the test.

    # Example of a mock object
    dilithium = mock()
    dilithium.expects(:nuke).with(:anti_matter).at_least_once
    
    enterprise = Enterprise.new(dilithium)
    enterprise.go(2)
    require 'test/unit'
    require 'mocha/test_unit'
    
    class EnterpriseTest < Test::Unit::TestCase
      def test_should_boldly_go
        dilithium = mock()
        dilithium.expects(:nuke).with(:anti_matter).at_least_once  # auto-verified at end of test
        enterprise = Enterprise.new(dilithium)
        enterprise.go(2)
      end
    end
  3. Understanding Stubs vs Expectations

    main

    In Mocha, stubs and expectations are fundamentally the same thing. An expectation is simply a stub with a defined cardinality (how many times it should be called).

    • Stub: An expectation of zero or more invocations. Use stubs to make the intent of the test explicit when you don't care how many times the method is called.
    • Expectation: Uses expects to assert that a method must be called.

    When a method is invoked on a mock, Mocha searches through expectations from newest to oldest to find a match. If an expectation matches but its cardinality is set to never, an unexpected invocation error is reported.

  4. Configure Mocha for Cucumber

    main

    To use Mocha with Cucumber, require mocha/api, register the API with the Cucumber World, and use an Around hook to manage the lifecycle of mocks and stubs.

    # In e.g. features/support/mocha.rb
    require 'mocha/api'
    
    World(Mocha::API)
    
    Around do |scenario, block|
      begin
        mocha_setup
        block.call
        mocha_verify
      ensure
        mocha_teardown
      end
    end
  5. Install Mocha via Gem

    main

    Install the latest version of the Mocha gem using the standard RubyGems command.

    Note: If using Mocha with Test::Unit or Minitest, you must set up Mocha after loading the relevant test library.

    $ gem install mocha
  6. How Mocha's core abstractions work together

    main

    Mocha is a mocking and stubbing framework built around several key abstractions that compose to allow fine-grained control over object behavior during tests:

    1. API: Provides the entry points for creating mocks and stubs. Methods from the API are automatically injected into Test::Unit::TestCase and Minitest::Unit::TestCase via adapters.
    2. Mock Creation: You create test doubles using mock, stub, or stub_everything. These methods return a Mock object.
    3. Expectations: Once you have a Mock, you use #expects (to define a required call) or #stubs (to define a method that can be called zero or more times). Both methods return an Expectation object.
    4. Expectation Fluent Interface: An Expectation can be further refined using a fluent interface to specify return values, error raising, or call counts.
    5. Parameter Matchers: You can use ParameterMatchers within Expectation#with to restrict which specific arguments will trigger a match for that expectation.

    Integration adapters provide built-in support for Minitest and Test::Unit, while integration hooks allow for support in other test frameworks.

  7. Manage Mocha lifecycle with Mockery.setup and teardown

    main

    Mocha uses Mocha::Mockery to manage the lifecycle of mocks, stubs, and expectations during a test run. To use Mocha methods, you must first initialize the environment using setup. After tests are complete, use teardown to clean up stubs and verify that all expectations were met.

    Note: Attempting to use Mocha methods (like creating mocks or stubs) before calling setup will raise a NotInitializedError with the message: "Mocha methods cannot be used outside the context of a test".

  8. Configure Mocha globally with Mocha.configure

    main

    You can set Mocha's configuration options globally, typically within a test_helper.rb or spec_helper.rb file. Use Mocha.configure to yield a configuration object that allows you to modify various settings.

    Mocha.configure do |c|
      c.stubbing_method_unnecessarily = :prevent
      c.stubbing_method_on_non_mock_object = :warn
    end
  9. Mocha Thread Safety Limitations

    main

    Mocha is not thread-safe.

    • Testing multi-threaded code: Not recommended. Exceptions raised in other threads may not be correctly intercepted by Mocha's error handling.
    • Running tests in parallel: Not recommended. Partial mocking modifies state in ObjectSpace, which is shared across all threads. If tests run concurrently, one test using any_instance will affect the behavior of other tests in the same process.
  10. Quick Start: Mocking and Stubbing Examples

    main

    Mocha provides several ways to create test doubles:

    • Mocking a class method: Use expects on the class.
    • Mocking an instance method on a real object: Use expects on the instance.
    • Stubbing instance methods on real objects: Use stubs on the instance.
    • Stubbing instance methods on all instances of a class: Use any_instance.stubs.
    • Traditional Mocking: Create a new mock object using mock('name').
    • Shortcuts: Create a stub object with predefined methods using stub(method: value).
    require 'test/unit'
    require 'mocha/test_unit'
    
    class MiscExampleTest < Test::Unit::TestCase
      def test_mocking_a_class_method
        product = Product.new
        Product.expects(:find).with(1).returns(product)
        assert_equal product, Product.find(1)
      end
    
      def test_mocking_an_instance_method_on_a_real_object
        product = Product.new
        product.expects(:save).returns(true)
        assert product.save
      end
    
      def test_stubbing_instance_methods_on_real_objects
        prices = [stub(pence: 1000), stub(pence: 2000)]
        product = Product.new
        product.stubs(:prices).returns(prices)
        assert_equal [1000, 2000], product.prices.collect {|p| p.pence}
      end
    
      def test_stubbing_an_instance_method_on_all_instances_of_a_class
        Product.any_instance.stubs(:name).returns('stubbed_name')
        product = Product.new
        assert_equal 'stubbed_name', product.name
      end
    
      def test_traditional_mocking
        object = mock('object')
        object.expects(:expected_method).with(:p1, :p2).returns(:result)
        assert_equal :result, object.expected_method(:p1, :p2)
      end
    
      def test_shortcuts
        object = stub(method1: :result1, method2: :result2)
        assert_equal :result1, object.method1
        assert_equal :result2, object.method2
      end
    end
  11. Configure strict keyword argument matching

    main

    In Ruby 3.0+, positional Hash arguments and keyword arguments are treated differently. To ensure your tests correctly distinguish between them and avoid misleading passes, you can enable strict matching.

    • Set strict_keyword_argument_matching = true to enforce strict comparison (requires Ruby 2.7+).
    • This is true by default in Ruby >= v3.0, but false by default in Ruby v2.7 to allow for gradual adoption.

    Warning: If you set this to false in Ruby >= v3.0, a deprecation warning will be displayed if a positional Hash matches a set of keyword arguments.

    Mocha.configure do |c|
      c.strict_keyword_argument_matching = true
    end
  12. Enable matching invocations on failure

    main

    By default, Mocha does not show which specific calls matched a stub when an expectation fails. You can enable this to see the actual arguments used in matching invocations alongside the unsatisfied expectations.

    Set display_matching_invocations_on_failure to true to enable this feature.

    Mocha.configure do |c|
      c.display_matching_invocations_on_failure = true
    end