You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Elixir/Pow Auth/ETS缓存测试报错:函数正常但测试失败

Pow重置密码测试中ETS缓存Mock初始化失败(ArgumentError: 无效选项)

手动测试重置密码控制器功能正常,但编写ExUnit测试用例时出现ETS初始化错误。已按照Pow官方文档配置ETS缓存Mock,但问题仍未解决。

控制器代码

@spec reset_password(Conn.t(), map()) :: Conn.t()
def reset_password(conn, %{"id" => token, "user" => user_params}) do
  with {:ok, conn} <- PowResetPassword.Plug.load_user_by_token(conn, token),
       {:ok, _user, conn} <- PowResetPassword.Plug.update_user_password(conn, user_params) do
    json(conn, %{status: "Password changed"})
  else
    {:error, _changeset, conn} ->
      json(conn, %{error: %{message: "Passwords are not the same"}})

    _ ->
      json(conn, %{error: %{message: "Expired Token"}})
  end
end

测试代码

setup do
  user = 
    %User{}
      |> User.changeset(%{email: "test@example.com", password: @password, password_confirmation: @password})
      |> Repo.insert!()

  {:ok, user: user}
end

describe "reset_password/2" do
  test "with valid token and passwords", %{conn: conn} do
    PowApiTemplate.Test.EtsCacheMock.init()
    pow_config = [otp_app: :pow_api_template]

    {:ok, %{token: token, user: user}, conn} = 
      conn
      |> Pow.Plug.put_config(Application.get_env(:pow_api_template, :pow))
      |> PowResetPassword.Plug.create_reset_token(%{"email" => "test@example.com"})

    valid_params = %{"id" => token, "user" => %{"password" => @new_password, "password_confirmation" => @new_password}}

    conn = post(conn, Routes.password_path(conn, :reset_password, valid_params))

    assert json = json_response(conn, 200)
  end
end

错误信息

1) test reset_password/2 with valid token and passwords (PowApiTemplateWeb.PasswordControllerTest)
    test/pow_api_template_web/controllers/password_controller_test.exs:45
    ** (ArgumentError) errors were found at the given arguments:
    
      * 2nd argument: invalid options
    
    code: PowApiTemplate.Test.EtsCacheMock.init()
    stacktrace:
      (stdlib 4.0.1) :ets.new(PowApiTemplate.Test.EtsCacheMock, [:set, :protected, :named_table])
      (pow_api_template 0.1.0) test/support/ets_cache_mock.ex:5: PowApiTemplate.Test.EtsCacheMock.init/0
      test/pow_api_template_web/controllers/password_controller_test.exs:46: (test)

已完成的配置

  1. conn_case.ex中的设置:
setup _tags do
  EtsCacheMock.init()

  {:ok, conn: Phoenix.ConnTest.build_conn(), ets: EtsCacheMock}
end
  1. 测试环境配置(config/test.exs):
config :pow_api_template, :pow,
  cache_backend: [cache_store_backend: PowApiTemplate.Test.EtsCacheMock]

修改为cache_store_backend: PowApiTemplate.Test.EtsCacheMock后问题依旧。
3. ETS缓存Mock模块:

defmodule PowApiTemplate.Test.EtsCacheMock do
  @moduledoc false
  @tab __MODULE__

  def init, do: :ets.new(@tab, [:ordered_set, :protected, :named_table])

  def get(config, key) do
    ets_key = ets_key(config, key)

    @tab
    |> :ets.lookup(ets_key)
    |> case do
      [{^ets_key, value} | _rest] -> value
      []                          -> :not_found
    end
  end

  def delete(config, key) do
    :ets.delete(@tab, ets_key(config, key))

    :ok
  end

  def put(config, record_or_records) do
    records     = List.wrap(record_or_records)
    ets_records = Enum.map(records, fn {key, value} ->
      {ets_key(config, key), value}
    end)

    send(self(), {:ets, :put, records, config})
    :ets.insert(@tab, ets_records)
  end

  def all(config, match) do
    ets_key_match = ets_key(config, match)

    @tab
    |> :ets.select([{{ets_key_match, :_}, [], [:"$_"]}])
    |> Enum.map(fn {[_namespace | keys], value} -> {keys, value} end)
  end

  defp ets_key(config, key) do
    [Keyword.get(config, :namespace, "cache")] ++ List.wrap(key)
  end
end

解决方案

1. 移除重复的Mock初始化调用

conn_case.ex的setup已经调用了EtsCacheMock.init(),测试代码里重复调用会导致第二次尝试创建同名ETS表时,因表已存在且类型不匹配(第一次是:ordered_set,第二次默认参数被覆盖为:set)触发ArgumentError。直接删除测试代码中的PowApiTemplate.Test.EtsCacheMock.init()行即可。

2. 优化Mock的init函数(可选)

为避免后续测试出现重复初始化问题,修改Mock的init函数,先检查表是否存在,存在则删除后再创建:

def init do
  if :ets.whereis(@tab) != :undefined do
    :ets.delete(@tab)
  end
  :ets.new(@tab, [:ordered_set, :protected, :named_table])
end

3. 修正Pow缓存配置

确保测试环境中Pow缓存后端配置正确:

# config/test.exs
config :pow_api_template, :pow,
  cache_backend: PowApiTemplate.Test.EtsCacheMock

如需传递额外配置选项,使用元组形式:

config :pow_api_template, :pow,
  cache_backend: {PowApiTemplate.Test.EtsCacheMock, [namespace: "pow_test_cache"]}

4. 简化测试中的配置传递

测试代码无需手动调用Pow.Plug.put_config,conn_case已构建带有正确Pow配置的conn:

# 修改后的测试代码
test "with valid token and passwords", %{conn: conn, user: user} do
  {:ok, %{token: token}, conn} = 
    conn
    |> PowResetPassword.Plug.create_reset_token(%{"email" => user.email})

  valid_params = %{"id" => token, "user" => %{"password" => @new_password, "password_confirmation" => @new_password}}

  conn = post(conn, Routes.password_path(conn, :reset_password, valid_params))

  assert json_response(conn, 200) == %{"status" => "Password changed"}
end

内容的提问来源于stack exchange,提问作者Mateusz Babski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 00:10:26