如何获取正在通过 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 添加了 inherited
、included
、extended
和 prepended
挂钩,并添加了打印语句,但它们从来没有叫。这让我完全不知所措,不知道引擎盖下到底是什么可怕的黑魔法机架测试。
总结一下:测试实例是在发送第一个请求时即时创建的。被测试的实例是由邪能魔法创建的,并躲避了所有用钩子抓住它的尝试,所以我找不到模拟它的方法。几乎感觉 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
我想获取正在通过 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 添加了 inherited
、included
、extended
和 prepended
挂钩,并添加了打印语句,但它们从来没有叫。这让我完全不知所措,不知道引擎盖下到底是什么可怕的黑魔法机架测试。
总结一下:测试实例是在发送第一个请求时即时创建的。被测试的实例是由邪能魔法创建的,并躲避了所有用钩子抓住它的尝试,所以我找不到模拟它的方法。几乎感觉 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