How do I stub things in MiniTest?

前端 未结 8 793
面向向阳花
面向向阳花 2020-12-02 09:46

Within my test I want to stub a canned response for any instance of a class.

It might look like something like:

Book.stubs(:title).any_instance().ret         


        
相关标签:
8条回答
  • 2020-12-02 10:07

    Just to further explicate @panic's answer, let's assume you have a Book class:

    require 'minitest/mock'
    class Book; end
    

    First, create a Book instance stub, and make it return your desired title (any number of times):

    book_instance_stub = Minitest::Mock.new
    def book_instance_stub.title
      desired_title = 'War and Peace'
      return_value = desired_title
      return_value
    end
    

    Then, make the Book class instantiate your Book instance stub (only and always, within the following code block):

    method_to_redefine = :new
    return_value = book_instance_stub
    Book.stub method_to_redefine, return_value do
      ...
    

    Within this code block (only), the Book::new method is stubbed. Let's try it:

      ...
      some_book = Book.new
      another_book = Book.new
      puts some_book.title #=> "War and Peace"
    end
    

    Or, most tersely:

    require 'minitest/mock'
    class Book; end
    instance = Minitest::Mock.new
    def instance.title() 'War and Peace' end
    Book.stub :new, instance do
      book = Book.new
      another_book = Book.new
      puts book.title #=> "War and Peace"
    end
    

    Alternatively, you can install the Minitest extension gem minitest-stub_any_instance. (Note: when using this approach, the Book#title method must exist before you stub it.) Now, you can say more simply:

    require 'minitest/stub_any_instance'
    class Book; def title() end end
    desired_title = 'War and Peace'
    Book.stub_any_instance :title, desired_title do
      book = Book.new
      another_book = Book.new
      puts book.title #=> "War and Peace"
    end
    

    If you want to verify that Book#title is invoked a certain number of times, then do:

    require 'minitest/mock'
    class Book; end
    
    book_instance_stub = Minitest::Mock.new
    method = :title
    desired_title = 'War and Peace'
    return_value = desired_title
    number_of_title_invocations = 2
    number_of_title_invocations.times do
      book_instance_stub.expect method, return_value
    end
    
    method_to_redefine = :new
    return_value = book_instance_stub
    Book.stub method_to_redefine, return_value do
      some_book = Book.new
      puts some_book.title #=> "War and Peace"
    # And again:
      puts some_book.title #=> "War and Peace"
    end
    book_instance_stub.verify
    

    Thus, for any particular instance, invoking the stubbed method more times than specified raises MockExpectationError: No more expects available.

    Also, for any particular instance, having invoked the stubbed method fewer times than specified raises MockExpectationError: expected title(), but only if you invoke #verify on that instance at that point.

    0 讨论(0)
  • 2020-12-02 10:10

    I thought I'd share an example that I built upon the answers here.

    I needed to stub a method at the end of a long chain of methods. It all started with a new instance of a PayPal API wrapper. The call I needed to stub was essentially:

    paypal_api = PayPal::API.new
    response = paypal_api.make_payment
    response.entries[0].details.payment.amount
    

    I created a class that returned itself unless the method was amount:

    paypal_api = Class.new.tap do |c|
      def c.method_missing(method, *_)
        method == :amount ? 1.25 : self
      end
    end
    

    Then I stubbed it in to PayPal::API:

    PayPal::API.stub :new, paypal_api do
      get '/paypal_payment', amount: 1.25
      assert_equal 1.25, payments.last.amount
    end
    

    You could make this work for more than just one method by making a hash and returning hash.key?(method) ? hash[method] : self.

    0 讨论(0)
  • 2020-12-02 10:20
      # Create a mock object
      book = MiniTest::Mock.new
      # Set the mock to expect :title, return "War and Piece"
      # (note that unless we call book.verify, minitest will
      # not check that :title was called)
      book.expect :title, "War and Piece"
    
      # Stub Book.new to return the mock object
      # (only within the scope of the block)
      Book.stub :new, book do
        wp = Book.new # returns the mock object
        wp.title      # => "War and Piece"
      end
    
    0 讨论(0)
  • 2020-12-02 10:20

    I use minitest for all my Gems testing, but do all my stubs with mocha, it might be possible to do all in minitest with Mocks(there is no stubs or anything else, but mocks are pretty powerful), but I find mocha does a great job, if it helps:

    require 'mocha'    
    Books.any_instance.stubs(:title).returns("War and Peace")
    
    0 讨论(0)
  • 2020-12-02 10:22

    You can easily stub class methods in MiniTest. The information is available at github.

    So, following your example, and using the Minitest::Spec style, this is how you should stub methods:

    # - RSpec -
    Book.stubs(:title).any_instance.returns("War and Peace")
    
    # - MiniTest - #
    Book.stub :title, "War and Peace" do
      book = Book.new
      book.title.must_equal "War and Peace"
    end
    

    This a really stupid example but at least gives you a clue on how to do what you want to do. I tried this using MiniTest v2.5.1 which is the bundled version that comes with Ruby 1.9 and it seems like in this version the #stub method was not yet supported, but then I tried with MiniTest v3.0 and it worked like a charm.

    Good luck and congratulations on using MiniTest!

    Edit: There is also another approach for this, and even though it seems a little bit hackish, it is still a solution to your problem:

    klass = Class.new Book do
      define_method(:title) { "War and Peace" }
    end
    
    klass.new.title.must_equal "War and Peace"
    
    0 讨论(0)
  • 2020-12-02 10:25

    If you're interesting in simple stubbing without a mocking library, then it's easy enough to do this in Ruby:

    class Book
      def avg_word_count_per_page
        arr = word_counts_per_page
        sum = arr.inject(0) { |s,n| s += n }
        len = arr.size
        sum.to_f / len
      end
    
      def word_counts_per_page
        # ... perhaps this is super time-consuming ...
      end
    end
    
    describe Book do
      describe '#avg_word_count_per_page' do
        it "returns the right thing" do
          book = Book.new
          # a stub is just a redefinition of the method, nothing more
          def book.word_counts_per_page; [1, 3, 5, 4, 8]; end
          book.avg_word_count_per_page.must_equal 4.2
        end
      end
    end
    

    If you want something more complicated like stubbing all instances of a class, then it is also easy enough to do, you just have to get a little creative:

    class Book
      def self.find_all_short_and_unread
        repo = BookRepository.new
        repo.find_all_short_and_unread
      end
    end
    
    describe Book do
      describe '.find_all_short_unread' do
        before do
          # exploit Ruby's constant lookup mechanism
          # when BookRepository is referenced in Book.find_all_short_and_unread
          # then this class will be used instead of the real BookRepository
          Book.send(:const_set, BookRepository, fake_book_repository_class)
        end
    
        after do
          # clean up after ourselves so future tests will not be affected
          Book.send(:remove_const, :BookRepository)
        end
    
        let(:fake_book_repository_class) do
          Class.new(BookRepository)
        end
    
        it "returns the right thing" do 
          # Stub #initialize instead of .new so we have access to the
          # BookRepository instance
          fake_book_repository_class.send(:define_method, :initialize) do
            super
            def self.find_all_short_and_unread; [:book1, :book2]; end
          end
          Book.find_all_short_and_unread.must_equal [:book1, :book2]
        end
      end
    end
    
    0 讨论(0)
提交回复
热议问题