如何在Elixir中对含模块调用副作用的函数编写单元测试?
如何测试Elixir控制器中调用外部模块的副作用函数
咱们来一步步拆解怎么给这个verify函数里的副作用写靠谱的单元测试。核心思路就是把外部依赖的模块Mock掉,这样不用真的操作数据库或者执行实际业务逻辑,就能精准验证副作用有没有被正确触发。
第一步:给外部依赖抽象出行为(Behaviour)
首先,你需要把Heimdallr.Commits这个模块的核心行为抽象成一个Behaviour,这样测试时可以用Mock来替代它。
先定义一个Behaviour模块:
defmodule Heimdallr.CommitsBehaviour do @callback touch_commit(Heimdallr.Commits.Commit.t()) :: any() end
然后让原Heimdallr.Commits模块实现这个Behaviour:
defmodule Heimdallr.Commits do @behaviour Heimdallr.CommitsBehaviour # ... 保留你原有的代码 @impl true def touch_commit(commit) do # 你的原有实现逻辑 end end
第二步:修改控制器,注入依赖而非硬编码
接下来要调整控制器代码,让它不直接硬调用Heimdallr.Commits,而是通过配置的模块来调用——这样测试时可以替换成Mock模块:
defmodule HeimdallrWeb.VerifyController do use HeimdallrWeb, :controller # 从应用环境读取依赖模块,默认用真实实现 @commits_module Application.compile_env(:heimdallr, :commits_module, Heimdallr.Commits) def verify(conn, _params) do [forwarded_host | _tail] = get_req_header(conn, "x-forwarded-host") case is_preview_page?(forwarded_host) do {:ok, false} -> conn |> send_resp(200, "") {:ok, %Heimdallr.Commits.Commit{} = commit} -> # 用注入的模块调用方法 @commits_module.touch_commit(commit) conn |> send_resp(200, "") {:not_found, _reason} -> ... end end end
第三步:用Mox编写测试用例
Elixir生态里最常用的Mock工具是Mox,咱们用它来写测试。
首先在test/test_helper.exs里定义Mock模块:
Mox.defmock(Heimdallr.CommitsMock, for: Heimdallr.CommitsBehaviour)
然后编写控制器的测试模块:
defmodule HeimdallrWeb.VerifyControllerTest do use HeimdallrWeb.ConnCase import Mox # 测试前把依赖换成Mock模块 setup do Application.put_env(:heimdallr, :commits_module, Heimdallr.CommitsMock) :ok end test "当请求头匹配预览页面且存在Commit时,触发touch_commit", %{conn: conn} do # 构造测试用的Commit实例 test_commit = %Heimdallr.Commits.Commit{id: 123} # 告诉Mox:我们期望touch_commit被调用一次,参数必须是这个test_commit expect(Heimdallr.CommitsMock, :touch_commit, fn ^test_commit -> :ok end) # 这里需要注意:如果`is_preview_page?`也是调用外部逻辑,你需要同样Mock它的返回 # 假设你已经处理了这个函数的Mock,让它返回{:ok, test_commit} conn = conn |> put_req_header("x-forwarded-host", "preview.example.com") |> get(Routes.verify_path(conn, :verify)) # 验证响应状态正确 assert conn.status == 200 # Mox会自动验证touch_commit是否按预期被调用,没调用的话测试会失败 end test "当请求头不匹配预览页面时,不会触发touch_commit", %{conn: conn} do # 告诉Mox:touch_commit绝对不能被调用(0次) expect(Heimdallr.CommitsMock, :touch_commit, 0, fn _ -> :ok end) # 让is_preview_page?返回{:ok, false} conn = conn |> put_req_header("x-forwarded-host", "normal.example.com") |> get(Routes.verify_path(conn, :verify)) assert conn.status == 200 # Mox会验证这个函数确实没被调用 end end
额外提示:处理is_preview_page?的分支
如果is_preview_page?也是调用了外部模块的函数,用同样的方法:把它抽象成Behaviour,注入到控制器里,测试时Mock它的返回值,这样就能覆盖所有分支(包括{:not_found, _reason}的情况)。
比如把is_preview_page?提取到一个单独的模块,定义Behaviour,然后在控制器里注入,测试时Mock不同的返回结果,就能完整覆盖三个分支的逻辑。
内容的提问来源于stack exchange,提问作者Samuel
相关产品推荐
相关产品推荐

