如何获取正在通过 rack-test 测试的 Sinatra 应用程序实例?

How do I get the Sinatra app instance that's being tested by rack-test?

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

我做了一个测试来证明我的问题(它需要 gem "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"))即时创建了测试实例,因此在此之前无法模拟应用程序实例。

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

这是输出:

From constructor: <TestSubject 47378836917780>
Proxy intercepted new! instance: <TestSubject 47378836917780>
Proxy intercepted new instance: <Sinatra::Wrapper 47378838065200>
From request handler: <TestSubject 47378838063980>

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

因此,令人困惑的是,被测试的实例从未创建,但不知何故存在 none 更少。我的猜测是 allocate 被使用了,但代理拦截显示它没有被使用。我 运行 TestSubject.allocate 亲自验证拦截是否有效,确实如此。

我还向测试的 class 添加了 inheritedincludedextendedprepended 挂钩,并添加了打印语句,但它们从来没有叫。这让我完全不知所措,不知道引擎盖下到底是什么可怕的黑魔法机架测试。

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

还在摸索解决办法

是的,机架测试会根据每个请求实例化新的 app(可能是为了避免冲突并从新状态开始。)这里的选项是模拟 Sinatra::Base 派生的 class 本身,里面 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 方法。

好的,我终于明白了。

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

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

注意: 我不能再在 app 函数中创建一个匿名的 class,因为那样会创建一个新的 class每次调用 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