Search code examples
ruby-on-railsrubyminiteststub

Stubbing with Ruby-on-Rails and MiniTest


I'm trying to understand how stubbing works with Rails and MiniTest. I've followed the simple example from MiniTest documentation. I'm stuck with a very simple example:

require 'minitest/mock'
require "test_helper"

class TotoTest < ActiveSupport::TestCase

  class Clazz
    def foo
      "foo"
    end
  end

  test "Stubbing" do
    puts Clazz.new.foo # "foo" is well printed
    Clazz.stub :foo, "bar" do # ERROR HERE
      assert_equal "bar", Clazz.new.foo
    end
  end
end

When stubbing, I get an error telling the method foo. The full execution log:

Testing started at 13:55 ...
[...]
Started

foo

Minitest::UnexpectedError: NameError: undefined method `foo' for class `TotoTest::Clazz'
    test/models/toto_test.rb:14:in `block in <class:TotoTest>'
test/models/toto_test.rb:14:in `block in <class:TotoTest>'
Finished in 0.52883s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

Process finished with exit code 0

I can't begin to understand why i'm told the foo method does not exist when the execution runs well the line before.

What am I missing? Why does this not work?

I've even tried an alternative, using a mock:

require 'minitest/mock'
require "test_helper"

class TotoTest < ActiveSupport::TestCase

  class Clazz
    def foo
      "foo"
    end
  end

  test "Stubbing" do
    mock = Minitest::Mock.new
    def mock.foo
      "bar"
    end

    puts Clazz.new.foo
    Clazz.stub :foo, mock do
      assert_equal "bar", Clazz.new.foo
    end
  end
end

The result is the same. Where am I wrong?

EDIT : Use case

To be more precise, I'd like to stub the YouTube API. The calls to YouTube API are implemented in a module. The module is included in a controller. In a system test, I'd like to replace the real call to that API by a stub, to be independent from YouTube API.


Solution

  • You're stubbing a class method instead of an instance method:

    Clazz.stub :foo, "bar"
    

    You call stub on an instance of Class class referenced by a constant Clazz.

    You should call #stub on Clazz instance:

    clazz = Clazz.new
    clazz.stub :foo, mock do
      assert_equal "bar", clazz.foo
    end
    

    Edit: Regarding the use case. I think that a controller is a wrong place to include methods handling an external API. I would suggest to wrap it in a separate object, then you can stub this object, e.g.:

    yt_mock = ... # mocking yt methods you want to use
    YouTube.stub :new, yt_mock do
      # controler test
    end
    

    You can also create YouTube as a class that accepts adapters and delegates calls to them - one adapter would use a real YT api, another one just predefined answers.