38

I want to test whether a function invokes other functions properly with minitest Ruby, but I cannot find a proper assert to test from the doc.

The source code
class SomeClass
  def invoke_function(name)
    name == "right" ? right () : wrong ()
  end

  def right
    #...
  end

  def wrong
    #...
  end
end
The test code:
describe SomeClass do
  it "should invoke right function" do
    # assert right() is called
  end

  it "should invoke other function" do
    # assert wrong() is called
  end
end
Flexoid
  • 4,155
  • 21
  • 20
steveyang
  • 9,178
  • 8
  • 54
  • 80

5 Answers5

29

Minitest has a special .expect :call for checking if some method is called.

describe SomeClass do
  it "should invoke right function" do
    mocked_method = MiniTest::Mock.new
    mocked_method.expect :call, return_value, []
    some_instance = SomeClass.new
    some_instance.stub :right, mocked_method do
      some_instance.invoke_function("right")
    end
    mocked_method.verify
  end
end

Unfortunately this feature is not documented very well. I found about it from here: https://github.com/seattlerb/minitest/issues/216

jvalanen
  • 2,809
  • 1
  • 16
  • 9
  • That's a shame how issue was closed. What does `return_value` mean here? – Nakilon Sep 06 '21 at 00:21
  • `return_value` is the value you want the `mocked_method` to return (instead of calling the original `mocked_method` implementation) – jvalanen Sep 07 '21 at 04:00
25

With minitest you use expect method to set the expectation for a method to be called on a mock object like so

obj = MiniTest::Mock.new
obj.expect :right

If you want to set expectation with parameters and return values then:

obj.expect :right, return_value, parameters

And for the concrete object like so:

obj = SomeClass.new
assert_send([obj, :right, *parameters])
nas
  • 3,676
  • 19
  • 19
  • 3
    For the concrete object version, there is `must_send` for when using `minitest/spec` – Frost Jul 18 '12 at 09:25
  • 1
    Tell me if I'm missing something but I think the answer by @jing-li below is correct in mentioning that `assert_send` isn't appropriate here. The OP wants to test that `right` is called when `invoke_function` is called. Additionally, `obj.verify` is needed to ensure `right` is called on the mock object in the first part of this answer. To anyone reading this, please ensure your tests fail before you make them pass. – monozok May 02 '16 at 17:35
  • I think using assert_send is not a safe choice. according to docs: http://ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/Assertions.html#method-i-assert_send – Filip Bartuzi Nov 25 '16 at 09:02
  • Can you elaborate your example on when is `invoke_function` called? – lulalala Nov 02 '17 at 12:04
  • The answer is completly incorrect, with a given code you are never tested that right was called. assert_send is just executing the right method, same as `assert( obj.right(*args) )` and expectation agains mock object is also incorrect suggestion to use Mock object like that you need to replace the original object somewhere in code with the mocker. – Алексей Лещук Jan 15 '22 at 14:52
3

According to the given example, there is no other delegate class, and you want to make sure the method is called properly from the same class. Then below code snippet should work:

class SomeTest < Minitest::Test
  def setup
    @obj = SomeClass.new
  end

  def test_right_method_is_called
    @obj.stub :right, true do
      @obj.stub :wrong, false do
        assert(@obj.invoke_function('right'))
      end
    end
  end

  def test_wrong_method_is_called
    @obj.stub :right, false do
      @obj.stub :wrong, true do
        assert(@obj.invoke_function('other'))
      end
    end
  end
end

The idea is to stub [method_expect_to_be_called] by returning a simple true value, and in the stub block assert it's indeed being called and returning the true value. To stub the other unexpected method is just to make sure that it's not being called.

Note: assert_send won't work correctly. Please refer to official doc.

In fact, below statement will pass, but doesn't mean it's working as expected:

assert_send([obj, :invoke_function, 'right'])
# it's just calling invoke_function method, but not verifying any method is being called
Jing Li
  • 14,547
  • 7
  • 57
  • 69
0

Recently I've created a gem for easing this kind of assertions called 'stubberry'.

Here how you can manage the needed behaviour using it.

First you need to answer the questions:

  • do you have an access to the original object before the test sequence execution?

  • is there any indirect way you can sure call happened? i.e. there should be some methods invocations on some other object you have access to.

  • do you need the method to be actually called or could it be stubbed with the prooper object or callable?

If you have access to the object, and you can stub the method, with your callable:

obj.stub_must( :right, -> { stub_response } ) {
  ... do something 
}

If you have access to the object but you don't want to stub the method and you just want to ensure that method was invoked:

  assert_method_called( obj, :right ) {
    .... do something with obj
  }

If you don't have the access to the object you want to test against. But you can do indirect check with some other object method invocation, Lets say 'right' method will end with API call execution:

API.stub_must( :get, -> (path, params) {
  assert_equal( path, :expected_path )
  assert_equal( params, {} )
} ) do
  ... do something  
end
  

If you can't do an indirect check:


stunt_boudle = Obj.new

stunt_boudle.stub_must( :right, -> () {
  #do needed assertions
} ) do
   Obj.stub_must(:new, stunt_boudle) do 
     # do some integration testing 
   end
end 

# OR use assert_method_called the same way

Also there is a cool set of stubbing ActiveRecord object by id, you can use them in this case when you can't have access to the object at the start of the testing actions and its an ActiveRecord object.

0

To stub and assert method calls, you use MiniTest::Mock. There are 2 ways to use this:

  • stub an object's method to return a mock object, which has a stubbed method
  • stub an object's method to call the mock method
test "return the mock object when calling the stubbed method" do
  # the object you want to stub
  obj = Book.new

  mock = MiniTest::Mock.new
  mock.expect :the_method_to_stub, "my cool return value"

  obj.stub :method_that_gives_you_a_mock, mock do
    x = obj.method_that_gives_you_a_mock
    assert_equal x.the_method_to_stub, "my cool return value"
  end

  # assert that the method was called once
  mock.verify
end
test "call the mock method when calling the stubbed method" do
  # the object you want to stub
  obj = Book.new

  mock = MiniTest::Mock.new
  # use :call to make the mock a callable
  mock.expect :call, "my cool return value"

  obj.stub :method_that_calls_the_mock, mock do
    assert_equal obj.method_that_calls_the_mock, "my cool return value"
  end

  # assert that the method was called once
  mock.verify
end

To use MiniTest::Mock, you may need to add require 'minitest/autorun' to load the MiniTest constants.

James Wong
  • 83
  • 1
  • 6