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



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

我已经进行了测试以证明我的问题(它需要宝石" sinatra"," rack-test"one_answers" 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")),因此在此之前不可能模拟App实例。

我已经使用RR的stub.proxy(...)拦截.new.new!.allocate;并添加了实例的类名和object_id的puts语句。我还在测试类的构造函数和请求处理程序中添加了此类语句。

这是输出:

来自构造函数:< testSubject 47378836917780>代理拦截了新!实例:< testSubject 47378836917780>代理拦截了新实例:< sinatra ::包装纸47378838065200>从请求处理程序:< testSubject 47378838063980>

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

因此,令人困惑的是,被测试的实例从未创建,但仍然存在一些。我的猜测是正在使用allocate,但是代理截距表明它没有。我自己运行了TestSubject.allocate来验证截距是否有效,并且确实有效。

我还向测试类添加了inheritedincludedextendedprepended钩子并添加了打印语句,但从未被调用。这使我完全且完全困惑,因为在引擎盖下进行了哪种可怕的黑魔法架检验。

因此,总结:发送第一个请求时,在Fly 上创建了测试的实例。经过测试的实例是由Fel Magic创建的,并躲避了所有用钩子捕捉它的尝试,因此我找不到嘲笑它的方法。几乎感觉像rake-test的作者已经延长了非凡的长度,以确保在测试过程中无法触摸应用程序实例。

我仍在四处寻找解决方案。

好吧,我终于得到了。

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

rack-test根本没有做任何令人费解的事情,它所称呼app()以检索应用程序,然后在应用程序上调用.call(env)。然后,我需要做的只是固执的我的班上的.call方法,并确保在任何地方都不会插入Sinatra的魔法。我可以在应用程序上使用.new!来阻止Sinatra插入包装纸和堆栈,并且我可以使用.call!来调用我的应用,而无需重复我的应用程序实例。

注意:我不能再在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

是的,机架测试每个请求实例化新的app(可能是为了避免碰撞并以新鲜状态开始。)此处的选项是模拟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方法。

最新更新