如何获取正在通过机架测试进行测试的 Sinatra 应用程序实例?

Hub*_*bro 5 ruby unit-testing rack rack-test

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

我做了一个测试来证明我的问题(它需要运行宝石“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
Run Code Online (Sandbox Code Playgroud)

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


更新

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

我用 rr'sstub.proxy(...)来拦截.new,.new!.allocate; 并添加了一个 puts 语句,其中包含实例的类名和object_id. 我还在测试类的构造函数和请求处理程序中添加了这样的语句。

这是输出:

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

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

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

我还添加了inheritedincludedextendedprepended挂钩的测试类,并添加打印语句,但他们从来没有所谓。这让我完全不知道在幕后进行了什么样的可怕的黑魔法机架测试。

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

我仍在摸索解决方案。

Hub*_*bro 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
Run Code Online (Sandbox Code Playgroud)