如何无需搭建真实数据库测试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

