【问题标题】:How do I get the Sinatra app instance that's being tested by rack-test?如何获取机架测试正在测试的 Sinatra 应用程序实例?
【发布时间】:2017-09-08 10:22:02
【问题描述】:

我想获取 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")),因此在此之前无法模拟应用程序实例。

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

这是输出:

来自构造函数: 代理拦截新!实例: 代理拦截新实例:<:wrapper> 来自请求处理程序:

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

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

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

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

我还在摸索解决办法。

【问题讨论】:

标签: ruby unit-testing rack rack-test


【解决方案1】:

好的,我终于明白了。

问题一直是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

【讨论】:

    【解决方案2】:

    是的,机架测试会为每个请求实例化新的 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 方法。

    【讨论】:

    • 它如何实例化新应用程序?我尝试过覆盖newnew!allocate,但它没有使用它们中的任何一个
    • 但是它如何从app 值创建新实例?正如我所说,我已经尝试加入类生命周期,但rake-test 创建的新实例并没有触发任何
    • 我不确定我是否遵循。在每个后续请求中,它都会调用app,从而产生一个新实例。所有请求都是delegated to it
    • 但如果就这么简单,为什么不在每个请求上调用#initialize
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多