How to assert certain method is called with Ruby minitest framework?
Asked Answered
G

6

39

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
Gaspard answered 3/6, 2012 at 10:6 Comment(0)
S
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])
Spilt answered 3/6, 2012 at 10:20 Comment(5)
For the concrete object version, there is must_send for when using minitest/specSeldom
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.Flume
I think using assert_send is not a safe choice. according to docs: ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/…Segovia
Can you elaborate your example on when is invoke_function called?Sustain
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.Ewan
H
33

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

Hummingbird answered 14/3, 2015 at 13:20 Comment(3)
That's a shame how issue was closed. What does return_value mean here?Dacoit
return_value is the value you want the mocked_method to return (instead of calling the original mocked_method implementation)Hummingbird
Change MiniTest to Minitest (notice lowercase "t") if you see "NameError: uninitialized constant xxx::MiniTest" error. Applies to the newer versions of Minitest.Coition
S
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])
Spilt answered 3/6, 2012 at 10:20 Comment(5)
For the concrete object version, there is must_send for when using minitest/specSeldom
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.Flume
I think using assert_send is not a safe choice. according to docs: ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/…Segovia
Can you elaborate your example on when is invoke_function called?Sustain
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.Ewan
D
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
Dentilabial answered 13/8, 2015 at 10:10 Comment(0)
H
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.

Haymaker answered 18/1, 2022 at 7:55 Comment(0)
B
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.

Boot answered 6/7, 2022 at 13:26 Comment(0)
C
0

March 2024 update:

You could use Minitest::Mock as follows:

mock = Minitest::Mock.new(delegator)
mock.expect(:method_name, 'return value', ['param1'])
assert_mock mock

where delegator is the object to which unexpected methods are delegated.

Changes from the previous versions of Minitest from the answers above:

  • MiniTest changed to Minitest (note lowercase "t").
  • Mock#expect requires return value argument.

Examples which also include a test that asserts method call using simple Fake object.


P.S. Based on your example you are actually testing the implementation instead of behavior. Doing so will greatly lowers maintainability of your application. You need to test whether your method does what it should do, not how.

Also, the only valid case when you need to test that a method is called is when your SUT calls other object's command method.

Coition answered 11/3 at 11:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.