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

时间:2017-09-08 10:22:03

标签: ruby unit-testing rack rack-test

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

我做了一个测试来证明我的问题(它需要宝石“sinatra”,“机架测试”和“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语句。我还在测试类的构造函数中添加了这样的语句,以及请求处理程序。

这是输出:

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

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

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

我还将inheritedincludedextendedprepended挂钩添加到测试类并添加了print语句,但它们从未被调用过。这让我完全彻底地难以理解为什么样的可怕的黑魔法机架测试在引擎盖下。

总结一下:当第一个请求发送时,测试的实例是动态创建的。被测试的实例是由fel magic创建的,并且闪避所有尝试用钩子抓住它,所以我找不到嘲笑它的方法。几乎感觉rake-test的作者已经付出了不同的代价,以确保在测试期间无法触及应用实例。

我仍在摸索寻求解决方案。

2 个答案:

答案 0 :(得分:0)

是的,机架测试为每个请求实例化新的app(可能是为了避免冲突并以新状态开始。)这里的选项是模拟Sinatra::Base派生类本身,在{{ 1}}:

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 方法。

答案 1 :(得分:0)

好的,我终于明白了。

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