我想抓住通过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
来验证拦截是否有效,而且确实如此。
我还将inherited
,included
,extended
和prepended
挂钩添加到测试类并添加了print语句,但它们从未被调用过。这让我完全彻底地难以理解为什么样的可怕的黑魔法机架测试在引擎盖下。
总结一下:当第一个请求发送时,测试的实例是动态创建的。被测试的实例是由fel magic创建的,并且闪避所有尝试用钩子抓住它,所以我找不到嘲笑它的方法。几乎感觉rake-test
的作者已经付出了不同的代价,以确保在测试期间无法触及应用实例。
我仍在摸索寻求解决方案。
答案 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