【问题标题】:Losing conn.assigns in the middle of a test在测试中间丢失 conn.assigns
【发布时间】:2018-09-01 20:00:18
【问题描述】:

我很难对失败的测试进行故障排除,其中 conn.assigns 都是同一测试语句的一部分,但在测试的两行之间被清空。

我正在阅读“Programming Phoenix”,并重新编写代码以使其适用于 Phoenix 1.3。在 3 个测试中,存储在 conn.assigns 中的 :current_user 在测试过程中丢失了,例如在控制器测试中的删除和获取之间。

测试代码为:

  describe "delete video" do
    setup [:login_user, :create_video]

    test "deletes chosen video", %{conn: conn, video: video} do
      conn = delete conn, video_path(conn, :delete, video)
      assert redirected_to(conn) == video_path(conn, :index)
      assert_error_sent 404, fn ->
        Logger.warn("Before get: #{inspect(conn.assigns)}")
        conn = get conn, video_path(conn, :show, video)
        Logger.warn("After get: #{inspect(conn.assigns)}")
        conn
      end
    end
  end

我添加了 Logger.warn 指令来检查获取请求之前和之后的连接。在“获取之前:”日志中,我仍然有一个带有 :current_user 键的 conn.assigns,但在随后的“获取之后”日志中,它消失了。

我最初认为我的身份验证机制对此负责,但后来我创建了一个日志插件来检查管道,并且我可以看到从调用 get 的管道开始,current_user 从分配中丢失。

这是显示管道开始/结束时的日志记录以及上述测试的日志的测试输出。

ubuntu@ubuntu-xenial:~/rumbl$ MIX_ENV=test mix test test/rumbl_web/controllers/video_controller_test.exs:98
[info] Already up
Including tags: [line: "98"]
Excluding tags: [:test]

warning: module attribute @update_attrs was set but never used
  test/rumbl_web/controllers/video_controller_test.exs:10

[debug] QUERY OK db=1.2ms
begin []
[debug] QUERY OK db=2.7ms
INSERT INTO "users" ("name","password_hash","username","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["Some User", "$2b$12$0VvTPna8JMdSXcbNz2uvwOdhYWF/3ibhQ.gntdXqsb8v1TSeCZ0.K", "max", {{2017, 9, 22}, {10, 56, 53, 527902}}, {{2017, 9, 22}, {10, 56, 53, 529912}}]
[debug] QUERY OK db=0.1ms
commit []
[debug] QUERY OK db=0.1ms
begin []
[debug] QUERY OK db=1.1ms
INSERT INTO "videos" ("description","title","url","user_id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["some description", "some title", "some url", 351, {{2017, 9, 22}, {10, 56, 53, 544069}}, {{2017, 9, 22}, {10, 56, 53, 544075}}]
[debug] QUERY OK db=0.1ms
commit []
[info] DELETE /manage/videos/274
[warn] Start of pipeline: %{current_user: %RumblWeb.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 351, inserted_at: ~N[2017-09-22 10:56:53.527902], name: "Some User", password: "supersecret", password_hash: "$2b$12$0VvTPna8JMdSXcbNz2uvwOdhYWF/3ibhQ.gntdXqsb8v1TSeCZ0.K", updated_at: ~N[2017-09-22 10:56:53.529912], username: "max", videos: #Ecto.Association.NotLoaded<association :videos is not loaded>}}
[warn] End of pipeline: %{current_user: %RumblWeb.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 351, inserted_at: ~N[2017-09-22 10:56:53.527902], name: "Some User", password: "supersecret", password_hash: "$2b$12$0VvTPna8JMdSXcbNz2uvwOdhYWF/3ibhQ.gntdXqsb8v1TSeCZ0.K", updated_at: ~N[2017-09-22 10:56:53.529912], username: "max", videos: #Ecto.Association.NotLoaded<association :videos is not loaded>}}
[warn] Start of authenticate_user: %{current_user: %RumblWeb.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 351, inserted_at: ~N[2017-09-22 10:56:53.527902], name: "Some User", password: "supersecret", password_hash: "$2b$12$0VvTPna8JMdSXcbNz2uvwOdhYWF/3ibhQ.gntdXqsb8v1TSeCZ0.K", updated_at: ~N[2017-09-22 10:56:53.529912], username: "max", videos: #Ecto.Association.NotLoaded<association :videos is not loaded>}}
[debug] Processing with RumblWeb.VideoController.delete/2
  Parameters: %{"id" => "274"}
  Pipelines: [:browser, :authenticate_user]
[debug] QUERY OK source="videos" db=0.9ms
SELECT v0."id", v0."description", v0."title", v0."url", v0."user_id", v0."category_id", v0."inserted_at", v0."updated_at" FROM "videos" AS v0 WHERE (v0."user_id" = $1) AND (v0."id" = $2) [351, 274]
[debug] QUERY OK db=0.1ms
begin []
[debug] QUERY OK db=0.3ms
DELETE FROM "videos" WHERE "id" = $1 [274]
[debug] QUERY OK db=0.0ms
commit []
[info] Sent 302 in 46ms
[warn] Before get: %{current_user: %RumblWeb.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 351, inserted_at: ~N[2017-09-22 10:56:53.527902], name: "Some User", password: "supersecret", password_hash: "$2b$12$0VvTPna8JMdSXcbNz2uvwOdhYWF/3ibhQ.gntdXqsb8v1TSeCZ0.K", updated_at: ~N[2017-09-22 10:56:53.529912], username: "max", videos: #Ecto.Association.NotLoaded<association :videos is not loaded>}}
[info] GET /manage/videos/274
[warn] Start of pipeline: %{}
[warn] End of pipeline: %{current_user: nil}
[warn] Start of authenticate_user: %{current_user: nil}
[info] Sent 302 in 2ms
[warn] After get: %{current_user: nil}


  1) test delete video deletes chosen video (RumblWeb.VideoControllerTest)
     test/rumbl_web/controllers/video_controller_test.exs:98
     expected error to be sent as 404 status, but response sent 302 without error
     code: assert_error_sent 404, fn ->
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:600: Phoenix.ConnTest.assert_error_sent/2
       test/rumbl_web/controllers/video_controller_test.exs:101: (test)



Finished in 0.6 seconds
8 tests, 1 failure, 7 skipped

Randomized with seed 982730

认证机制遵循书中的建议,如果 conn.assigns 中有 :current_user 则通过认证。设置 login_user 就是这样做的,在 :current_user 键下的 conn.assigns 中注入一个用户。在我的测试中,无论出于何种原因,由于 conn.assigns 在 get 请求中的某个时间点为空,身份验证失败,并且用户被重定向到主页。

我不确定如何解决删除语句和后续获取之间发生的问题。我查看了管道,但正如在日志语句中显示的那样, conn.assigns 在进入管道之前就被清空了。我原以为 conn 不会在整个测试语句中发生变化,也许这是一个错误的假设。

回复 Dogbert 的评论:

控制器测试中的login_user函数:

  defp login_user(_) do
    user = insert_user(username: "max")
    conn = assign(build_conn(), :current_user, user)
    {:ok, conn: conn, user: user}
  end

在授权控制器中调用函数:

  def call(conn, repo) do
    user_id = get_session(conn, :user_id)

    cond do
      user = conn.assigns[:current_user] ->
        conn
      user    = user_id && repo.get(RumblWeb.User, user_id) ->
        assign(conn, :current_user, user)
      true ->
        assign(conn, :current_user, nil)
    end
  end

【问题讨论】:

  • 你能把login_user的代码贴出来吗?这可能与这个问题有关:stackoverflow.com/q/42347723/320615.
  • 我添加了 login_user 函数,以及来自我的 Auth 控制器的调用函数。除非我遗漏了某些东西,否则“条件”应该确保如果设置了 :current_user,我不会覆盖我的分配。
  • user = insert_user(username: "max") 之后添加另一个检查 - 你真的在这里有一个用户,还是只有一个nil

标签: elixir phoenix-framework


【解决方案1】:

这是凤凰回收造成的,见(https://hexdocs.pm/phoenix/1.3.0/Phoenix.ConnTest.html#module-recycling)

回收

浏览器使用 cookie 实现存储。当响应中设置了 cookie 时,浏览器会将其存储并在下一个请求中发送。

为了模拟这种行为,这个模块提供了回收的想法。 recycle/1 函数接收一个连接并返回一个新连接,类似于 conn/0 返回的连接,将来自前一个连接的所有响应 cookie 定义为请求标头。这在测试需要 cookie 或会话才能工作的多个路由时很有用。

请记住,Phoenix 会自动回收分派之间的连接。这通常在大多数情况下都能正常工作,但如果您在下一次分派之前修改连接,它可能会丢弃信息:

# No recycling as the connection is fresh
conn = get build_conn(), "/"

# The connection is recycled, creating a new one behind the scenes
conn = post conn, "/login"

# We can also recycle manually in case we want custom headers
conn =
  conn
  |> recycle()
  |> put_req_header("x-special", "nice")

# No recycling as we did it explicitly
conn = delete conn, "/logout"

回收也回收“接受”标头。

要保留assigns,您可以在deleteget 的调用之间手动回收和分配:

saved_assigns = conn.assigns
conn = 
  conn
  |> recycle()
  |> Map.put(:assigns, saved_assigns)

【讨论】:

  • 迈克,有什么办法可以防止这种回收吗?还是将 :current_user 放入 conn.assigns 的概念不再有效?这是“Programming Phoenix 1.0”为了让测试更容易而提出的一个小秘籍。我应该先恢复进行适当的身份验证,还是有办法保持技巧并绕过回收?
  • 回收是个问题。这让我疯狂了好几天,现在我的代码周围都有 Logger 语句和一个丑陋的回收块,我需要将它放入一个函数中以便于重用。但我学到了一些东西,我的测试通过了。非常感谢您的帮助!
  • 这真的还是最好的选择吗?看起来太恶心了这就像一种气味,但使用 Auth 测试控制器并不是一种气味……
  • 我认为您不希望在最后一个代码块中出现|&gt; Map.put(&amp;1, :assigns, ...。使用 |> conn 将作为第一个参数传递,所以应该是 |&gt; Map.put(:assigns, ...
  • 感谢@WillW,现已修复。
猜你喜欢
  • 2017-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-14
  • 2014-08-23
  • 1970-01-01
相关资源
最近更新 更多