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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 03:54:14