【问题标题】:How do I stub things in MiniTest?我如何在 MiniTest 中存根东西?
【发布时间】:2011-11-04 21:09:06
【问题描述】:

在我的测试中,我想为类的任何实例存根预设响应。

它可能看起来像:

Book.stubs(:title).any_instance().returns("War and Peace")

然后每当我打电话给@book.title 它都会返回“战争与和平”。

有没有办法在 MiniTest 中做到这一点? 如果是的话,你能给我一个示例代码sn-p吗?

或者我需要摩卡之类的东西吗?

MiniTest 确实支持 Mocks,但 Mocks 对于我需要的东西来说太过分了。

【问题讨论】:

    标签: ruby minitest stub


    【解决方案1】:
      # 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
    

    【讨论】:

    • 我觉得这很难读。意图尚不清楚。虽然这更像是对 minitest 的评论而不是您的答案。
    • @StevenSoroka, here 在我自己的回答中,我详细阐述了恐慌的回答。
    • @StevenSoroka 同样。我添加了 cmets 以使不习惯 Minitest 语法的人更清楚。
    【解决方案2】:

    我在所有 Gems 测试中都使用 minitest,但我所有的存根都用 mocha 做,可能在 minitest 中用 Mocks 做所有事情(没有存根或其他任何东西,但 mock 非常强大),但我发现如果有帮助,mocha 做得很好:

    require 'mocha'    
    Books.any_instance.stubs(:title).returns("War and Peace")
    

    【讨论】:

      【解决方案3】:

      如果您对没有模拟库的简单存根感兴趣,那么在 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
      

      如果你想要一些更复杂的事情,比如对一个类的所有实例进行存根,那么它也很容易做到,你只需要有一点创意:

      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
      

      【讨论】:

      • 如果您在测试中重新定义一个方法,它会在测试完成后立即恢复还是为其他测试保持重新定义?
      【解决方案4】:

      您可以轻松地在MiniTest 中存根类方法。该信息可在github 获得。

      因此,按照您的示例,并使用 Minitest::Spec 样式,这就是您应该如何存根方法:

      # - 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
      

      这是一个非常愚蠢的例子,但至少为您提供了如何做您想做的事情的线索。我使用 MiniTest v2.5.1 进行了尝试,这是 Ruby 1.9 附带的捆绑版本,似乎在此版本中尚不支持 #stub 方法,但随后我尝试了 MiniTest v3.0,它就像一个魅力。

      祝您好运并祝贺您使用 MiniTest

      编辑:还有另一种方法可以解决这个问题,尽管它看起来有点老套,但它仍然可以解决您的问题:

      klass = Class.new Book do
        define_method(:title) { "War and Peace" }
      end
      
      klass.new.title.must_equal "War and Peace"
      

      【讨论】:

      • 看起来Book.stub :title, "War and Peace" 仅在titleBook 的class_method 时才有效我无法重现与any_instance 相同的行为,Book' 的错误NameError: undefined method title'
      • 未定义方法标题'为书'
      • stub 按照 MiniTest 中的 the source of the mock librarymetaclass = class << self; self; end 上运行。所以看起来你只能存根单例方法;否则,请使用完整的模拟对象。
      • 在我的 Rails 应用程序中使用它时出现名称错误。我正在添加require 'minitest/mock',但仍然得到NoMethodError: undefined method stub'`
      • @TomRossi 看看这个:github.com/seattlerb/minitest/issues/384 1.9.3 附带的 minitest 版本不包括存根。您必须加载更高版本的 minitest/mock
      【解决方案5】:

      为了进一步解释 @panic's answer,假设您有一个 Book 类:

      require 'minitest/mock'
      class Book; end
      

      首先,创建一个 Book 实例存根,并使其返回您想要的标题(任意次数):

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

      然后,让 Book 类实例化您的 Book 实例存根(仅且始终在以下代码块中):

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

      在此代码块中(仅),Book::new 方法被存根。让我们试试吧:

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

      或者,最简洁的:

      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
      

      或者,您可以安装 Minitest 扩展 gem minitest-stub_any_instance。 (注意:使用这种方法时,Book#title 方法必须在你存根之前存在。)现在,你可以更简单地说:

      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
      

      如果您想验证Book#title 是否被调用了一定次数,请执行以下操作:

      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
      

      因此,对于任何特定实例,调用存根方法的次数超过指定次数会引发 MockExpectationError: No more expects available

      此外,对于任何特定实例,调用存根方法的次数少于指定次数会引发 MockExpectationError: expected title(),但前提是您当时在该实例上调用 #verify

      【讨论】:

        【解决方案6】:

        您不能存根类的所有个实例,但您可以存根给定对象的任何实例方法,如下所示:

        require "minitest/mock"
        
        book = Book.new
        book.stub(:title, 'War and Peace') do
          assert_equal 'War and Peace', book.title
        end
        

        【讨论】:

          【解决方案7】:

          您始终可以在您的测试代码中创建一个模块,并使用包含或扩展来对猴子补丁类或对象使用它。例如(在 book_test.rb 中)

          module BookStub
            def title
               "War and Peace"
            end
          end
          

          现在您可以在测试中使用它

          describe 'Book' do
            #change title for all books
            before do
              Book.include BookStub
            end
          end
          
           #or use it in an individual instance
           it 'must be War and Peace' do
             b=Book.new
             b.extend BookStub
             b.title.must_equal 'War and Peace'
           end
          

          这允许您将比简单存根可能允许的更复杂的行为组合在一起

          【讨论】:

          【解决方案8】:

          我想我会分享一个基于此处答案构建的示例。

          我需要在一长串方法的末尾存根一个方法。这一切都始于一个 PayPal API 包装器的新实例。我需要存根的调用本质上是:

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

          我创建了一个返回自身的类,除非方法是amount

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

          然后我把它存根到PayPal::API

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

          您可以通过创建哈希并返回 hash.key?(method) ? hash[method] : self 来使这项工作不止一种方法。

          【讨论】:

            猜你喜欢
            • 2015-10-14
            • 2020-10-07
            • 1970-01-01
            • 2016-10-01
            • 2016-10-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多