5

我想掌握 rack-test 正在测试的应用程序实例,以便我可以模拟它的一些方法。我以为我可以简单地将应用程序实例保存在app方法中,但由于某些奇怪的原因,它不起作用。似乎rack-test只是使用实例来获取类,然后创建自己的实例。

我做了一个测试来证明我的问题(它需要运行 gems “sinatra”、“rack-test”和“rr”):

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "instantiated app" do
  include Rack::Test::Methods

  def app
    cls = Class.new(Sinatra::Base) do
      get "/foo" do
        $instance_id = self.object_id

        generate_response
      end

      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am a response"]
      end
    end

    # Instantiate the actual class, and not a wrapped class made by Sinatra
    @app = cls.new!

    return @app
  end

  it "should have the same object id inside response handlers" do
    get "/foo"

    assert_equal $instance_id, @app.object_id,
      "Expected object IDs to be the same"
  end

  it "should trigger mocked instance methods" do
    mock(@app).generate_response {
      [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
    }

    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end

怎么rack-test不使用我提供的实例?如何获取rack-test正在使用的实例,以便模拟该generate_response方法?


更新

我没有取得任何进展。事实证明,rack-test在发出第一个请求(即get("/foo"))时会即时创建测试实例,因此在此之前无法模拟应用程序实例。

我用 rrstub.proxy(...)截取.new,.new!.allocate; 并添加了带有实例类名的 puts 语句和object_id. 我还在测试类的构造函数以及请求处理程序中添加了此类语句。

这是输出:

来自构造函数:<TestSubject 47378836917780>
代理拦截新!实例:<TestSubject 47378836917780>
代理拦截新实例:<Sinatra::Wrapper 47378838065200>
来自请求处理程序:<TestSubject 47378838063980>

注意对象 ID。测试实例(从请求处理程序打印)从未通过.new,也从未初始化。

因此,令人困惑的是,被测试的实例从未被创建,但不知何故仍然存在。我的猜测是allocate正在使用它,但代理拦截显示它没有。我跑TestSubject.allocate自己来验证拦截是否有效,它确实有效。

我还在测试的类中添加了inherited、和钩子included,并添加了打印语句,但它们从未被调用过。这让我完全不知道引擎盖下有什么样的可怕的黑魔法机架测试。extendedprepended

总结一下:测试实例是在发送第一个请求时动态创建的。被测试的实例是由邪能魔法创建的,并且躲过了所有用钩子抓住它的尝试,所以我找不到模拟它的方法。几乎感觉作者rake-test已经竭尽全力确保在测试期间不能触及应用程序实例。

我仍在摸索解决方案。

4

2 回答 2

3

好的,我终于明白了。

问题,一直以来,原来是Sinatra::Base.call。在内部,它确实如此dup.call!(env)。换句话说,每次运行时call,Sinatra 都会复制您的应用程序实例并将请求发送到副本,从而绕过所有模拟和存根。这就解释了为什么没有触发任何生命周期钩子,因为大概dup使用了一些低级 C 魔法来克隆实例(需要引用。)

rack-test根本不做任何令人费解的事情,它所做的一切都是app()为了检索应用程序,然后调用.call(env)应用程序。然后,我需要做的就是.call在我的班级中删除该方法,并确保 Sinatra 的魔法没有被插入任何地方。我可以.new!在我的应用程序上使用来阻止 Sinatra 插入包装器和堆栈,并且我可以使用它.call!来调用我的应用程序,而无需 Sinatra 复制我的应用程序实例。

注意:我不能再在app函数内创建一个匿名类,因为每次app()调用都会创建一个新类,让我无法模拟它。

这是来自问题的测试,更新后可以工作:

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "sinatra app" do
  include Rack::Test::Methods

  class TestSubject < Sinatra::Base
    get "/foo" do
      generate_response
    end

    def generate_response
      [200, {"Content-Type" => "text/plain"}, "I am a response"]
    end
  end

  def app
    return TestSubject
  end

  it "should trigger mocked instance methods" do
    stub(TestSubject).call { |env|
      instance = TestSubject.new!

      mock(instance).generate_response {
        [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
      }

      instance.call! env
    }

    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end
于 2017-09-09T12:26:47.607 回答
0

是的, rack testapp为每个请求实例化 new (可能是为了避免冲突并从一个新状态开始。)这里的选项是在Sinatra::Base里面模拟派生类本身app

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "instantiated app" do
  include Rack::Test::Methods

  def app
    Class.new(Sinatra::Base) do
      get "/foo" do
        generate_response
      end

      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am a response"]
      end
    end.prepend(Module.new do # ⇐ HERE
      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
      end
    end).new!
  end

  it "should trigger mocked instance methods" do
    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end

或者,整体模拟app方法。

于 2017-09-08T12:41:04.120 回答