在Sinatra中,如何测试可以return同时html和json的路由?

In Sinatra, how to test a route that can return both html and json?

我对 Sinatra 还是个新手,我的应用程序全部基于 json 构建,没有任何视图。现在我想有相同的行为,但在视图上呈现结果。当我刚刚 returning json 时,我的测试都成功了。现在我试图在路由中引入 erb 模板,我的测试崩溃了,路由方法中的变量也没有传递给视图。

我做错了什么?

测试代码如下:

main_spec.rb:

describe "when a player joins the game" do

  it "welcomes the player" do
    post "/join", "data" => '{"name": "Jon"}'
    response = {status: Messages::JOIN_SUCCESS}
    expect_response_to_eq(response)
  end

  it "sends an error message if no name is sent" do
    post "/join", "data" => '{}'
    response = {status: Messages::JOIN_FAILURE}
    expect_response_to_eq(response)
  end

  it "sends an error message if player could not join the game" do
    fill_the_game
    post "/join", "data" => '{"name": "Jon"}'
    response = {status: Messages::JOIN_FAILURE}
    expect_response_to_eq(response)
  end

  it "returns an empty response if no data is sent" do
    post "/join"
    expect_response_to_eq({})
  end

  def expect_response_to_eq(response)
    expect(last_response).to be_ok
    expect(JSON.parse(last_response.body, symbolize_names: true)).to eq(response)
  end

  def fill_the_game
    server.join_game("Jane")
    server.join_game("Joe")
    server.join_game("Moe")
    server.join_game("May")
  end

end

其中 Messages 是一个包含游戏字符串消息的模块。

我尝试测试的控制器方法最初看起来像这样,它只是 returned json 格式的响应:

main.rb

post "/join" do
  response = helper.join_response(params)
  @title   = Messages::JOIN_TITLE
  response.to_json
end

助手是一个 class,我在其中提取了所有业务逻辑,以便控制器只需处理 HTTP 请求。我使用依赖注入将助手传递给主控制器,这样更容易测试。

到目前为止,如果我 运行 测试,它们是绿色的。但是现在我想通过 erb 在视图中呈现响应的结果,同时仍然 returning json。所以我添加了这样的测试:

main_spec.rb:

  it "renders the join page" do
    h = {'Content-Type' => 'text/html'}
    post "/join", "data" => '{"name": "Jon"}', "headers" => h
    expect(last_response).to be_ok
    expect(last_response.body).to include(Messages::JOIN_TITLE)
  end

然后修改加入路由器使测试通过(为此我需要 sinatra-contrib):

main.rb:

post "/join", :provides => ['html', 'json'] do
  response  = helper.join_response(params)
  @title    = Messages::JOIN_TITLE
  @r_status = response[:status]

  respond_to do |format|
    format.html { erb :join }
    format.json { response.to_json }
  end

  response.to_json
end

这打破了我所有的测试并显示消息:

Failure/Error: expect(last_response).to be_ok
       expected `#<Rack::MockResponse:0x007f89e9a941e0...` 
       # ommited this part due to space constrains
       `...@body=["<h1>Internal Server Error</h1>"]>.ok?` to return true, got false

所以我尝试了其他方法:

main.rb:

post "/join", :provides => ['html', 'json'] do
  response  = helper.join_response(params)
  @title    = Messages::JOIN_TITLE
  @r_status = response[:status]

  request.accept.each do |type|
    case type.to_s
    when 'text/html'
      halt erb :join
    when 'text/json'
      halt response.to_json
    end
  end

end

也用不同的消息破坏了一切:

Failure/Error: expect(JSON.parse(last_response.body, symbolize_names: true)).to eq(response)

JSON::ParserError:
  784: unexpected token at '*/*'

如果我在末尾添加一行,response.to_json,就在关闭方法之前,除了视图测试的最后一行 expect(last_response.body).to include(Messages::JOIN_TITLE),我的测试都通过了。事实上,当我在浏览器中加载页面时,@title 似乎被发送到页面而不是 @r_status 由于某种原因。在 erb 视图中,我有 <p><%= @r_status %></p>,所以它应该显示出来。标题在布局 erb 中呈现为 <h1><%= @title %></h1>.

我已经打印了 @r_status 的值并且它是正确的,但是如果我从 when 块内部打印东西,就好像它永远不会碰到那些。

我做错了什么? 为什么 @r_status 没有在视图中呈现,为什么 when 块没有被命中?我怎样才能使控制器 return 同时成为 html 或 json 最重要的是,我该如何测试它?

更新:

我已经更新了第一个测试以发送特定的 headers:

it "welcomes the player" do
  h = {"Content-Type" => "application/json", "Accept" => "application/json"}
  post "/join", "data" => '{"name": "Jon"}', "headers" => h
  response = {status: Messages::JOIN_SUCCESS}
  expect_response_to_eq(response)
end

我运行就是那个测试

$ rspec spec/main_spec.rb:20

当我从控制器打印请求信息时,这是我得到的:

p request.content_type
application/x-www-form-urlencoded

p request.accept
[#<Sinatra::Request::AcceptEntry:0x007f86de1566b8 @entry="*/*", @type="*/*", @params={}, @q=1.0>]

p params
{"data"=>"{\"name\": \"Jon\"}", "headers"=>{"Content-Type"=>"application/json", "Accept"=>"application/json"}}

似乎它没有占用我的 headers... 所以当我 request.accept.each do |type| case type.to_s 它 returns */* 两者都不匹配 html 也不是 json,这就是为什么它从不 运行 when 语句中的代码。我应该使用我的参数在路由方法中手动重定向到 erb 或 json 吗?我该怎么做?

第一个测试失败是因为应用程序中发生错误,因此您需要检查日志以查看是什么触发了它。 (你是否正在加载 Sinatra-Contrib 的 RespondWith 模块,因为 respond_to 不是 Sinatra 提供的方法)。

第二个示例的错误是因为 header 测试和应用程序之间的期望不匹配。在应用程序中,您正在使用 request.accept.each 迭代请求发送到您的应用程序的 Accept headers,默认情况下为 '/',但测试发送 Content-Type 的 header 不会触发条件 case 语句。

我发现了错误。正如我所怀疑的那样,问题出在 header 中。我没有正确发送它们。

根据 the docs,这是在测试中使用的 HTTP 方法的签名:

get '/path', params={}, rack_env={}

所以我的测试没有成功,因为第三个参数是 a rack environment variable 而我在那里传递了 headers:

h = {'Content-Type' => 'text/html'}
post "/join", "data" => '{"name": "Jon"}', "headers" => h

相反,我必须在调用该方法之前分别添加每个 header 调用 header(header_name, header_value)。所以现在我的 JSON 测试看起来像这样:

  it "welcomes the player" do
    header "Accept", "application/json"
    post "/join", {"name" => "Jon"}
    response = {status: Messages::JOIN_SUCCESS}
    expect_response_to_eq(response)
  end

我的视图测试如下所示:

  it "renders the join page" do
    header "Accept", "text/html"
    post "/join", {"name" => "Jon"}
    expect(last_response).to be_ok
    expect(last_response.body).to include(Messages::JOIN_TITLE)
  end

我在 main.rb 中的生产代码有效:

  post "/join", :provides => ['html', 'json'] do
    response  = helper.join_response(params)
    @title    = Messages::JOIN_TITLE
    @r_status = response[:status]

    request.accept.each do |type|
      case type.to_s
      when 'text/html'
        halt erb :join
      when 'application/json'
        halt response.to_json
      end
    end
    error 406
  end

如果我打印 headers 我得到:

p headers  
{"Content-Type"=>"application/json"}        # for JSON
{"Content-Type"=>"text/html;charset=utf-8"} # for the views
{"Content-Type"=>"text/html;charset=utf-8"} # in the browser

如果我根据请求打印它们,我会得到:

p request.accept
# for JSON:
[#<Sinatra::Request::AcceptEntry:0x007fcf973ff480 @entry="application/json", @type="application/json", @params={}, @q=1.0>]
# for the views:
[#<Sinatra::Request::AcceptEntry:0x007f5791fe29d8 @entry="text/html", @type="text/html", @params={}, @q=1.0>]
# in the browser:
<Sinatra::Request::AcceptEntry:0x007f2fd5a0ee50 @entry="text/html", @type="text/html", @params={}, @q=1.0>