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

如何无需搭建真实数据库测试Phoenix控制器?能否Mock函数?

Phoenix控制器测试:Mock复杂数据库函数避免重复前置设置

你已经完成了Api.Students.list_students/1的数据库测试(包含多表插入的复杂前置),现在要测试StudentController.index/2和视图逻辑,完全可以在不修改原业务代码的情况下Mock这个函数,避免重复繁琐的数据库前置工作。下面是具体实现方案:

核心思路

利用Elixir的Mox库(Phoenix生态常用的Mock工具),通过定义行为(Behaviour)抽象list_students/1的接口,然后在测试中Mock该行为的实现,替代原模块的数据库查询逻辑。

具体步骤

1. 添加Mox依赖

在mix.exs的deps中加入Mox:

defp deps do
  [
    # 其他依赖...
    {:mox, "~> 1.0", only: :test}
  ]
end

执行mix deps.get安装依赖。

2. 配置Mox

在config/test.exs末尾添加:

Mox.defmock(Api.StudentsMock, for: Api.StudentsBehaviour)

3. 定义行为(Behaviour)

在lib/api/students/behaviour.ex中创建行为文件,定义list_students/1的接口:

defmodule Api.StudentsBehaviour do
  @callback list_students(integer()) :: list(Api.Students.StudentSemester.t())
end

修改原Api.Students模块,让它实现这个行为(仅需加一行@behaviour声明,不改动原有逻辑):

defmodule Api.Students do
  @behaviour Api.StudentsBehaviour

  alias Api.Repo
  alias Api.Students.StudentSemester
  alias Api.Students.Student

  @impl true
  def list_students(semester_id) do
    from(ss in StudentSemester,
      join: s in Student,
      on: ss.semester_id == ^semester_id and ss.student_id == s.user_id,
      join: u in assoc(s, :user),
      preload: [student: {s, user: u}],
      select: ss
    )
    |> Repo.all()
  end
end

4. 在控制器测试中Mock行为

在test/api_web/controllers/student_controller_test.exs中编写测试,通过Mox替换Api.Students的调用:

defmodule ApiWeb.StudentControllerTest do
  use ApiWeb.ConnCase

  import Mox

  # 确保每个测试后验证Mock调用是否符合预期
  setup :verify_on_exit!

  test "GET /semesters/:semester_id/students returns students", %{conn: conn} do
    # 1. 构造和真实返回结构一致的模拟数据
    mock_students = [
      %Api.Students.StudentSemester{
        id: 1,
        semester_id: 123,
        student: %Api.Students.Student{
          user_id: 456,
          user: %Api.Users.User{id: 456, name: "Test Student"}
        }
      }
    ]

    # 2. 设置Mock规则:调用list_students(123)时返回模拟数据
    expect(Api.StudentsMock, :list_students, fn 123 -> mock_students end)

    # 3. 临时替换业务模块为Mock(不修改原代码)
    Application.put_env(:api, :students_module, Api.StudentsMock)

    # 4. 发起请求测试控制器逻辑
    conn = get(conn, "/semesters/123/students")
    assert json_response(conn, 200)["students"] != []

    # 5. 恢复原配置,避免影响其他测试
    Application.delete_env(:api, :students_module)
  end
end

5. 无侵入式调整控制器(可选)

如果不想修改原控制器代码,可通过依赖注入动态获取模块:
修改StudentController的index/2函数,从应用配置中读取业务模块:

def index(conn, %{"semester_id" => semester_id}) do
  students_module = Application.get_env(:api, :students_module, Api.Students)
  students = students_module.list_students(semester_id)
  render(conn, "index.json", students: students)
end

生产环境默认使用Api.Students,测试环境自动切换为Mock,完全无侵入。

替代方案:用meck直接Mock模块

如果不想引入Mox,可使用Erlang原生的meck库直接Mock模块:

test "GET /semesters/:semester_id/students returns students", %{conn: conn} do
  mock_students = [/* 模拟数据 */]
  :meck.new(Api.Students, [:non_strict])
  :meck.expect(Api.Students, :list_students, fn 123 -> mock_students end)

  conn = get(conn, "/semesters/123/students")
  assert json_response(conn, 200)["students"] != []

  :meck.unload(Api.Students)
end

但Mox更符合Elixir的行为规范,可读性和可维护性更强,推荐优先使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 02:43:28