Elixir ExUnit测试无法捕获Task.Supervisor子进程IO输出问题
问题根因
三个核心问题导致捕获失败:
capture_log仅能捕获ElixirLogger模块产生的日志输出,根本无法捕获IO.puts/2产生的标准IO输出,标准IO输出需要用capture_io捕获。- Erlang虚拟机的IO输出由进程的
group leader(组领导进程)负责路由,capture_io仅能捕获当前测试进程作为group leader管辖链路下的IO内容。你通过Task.Supervisor启动的子任务,默认继承Task.Supervisor进程的group leader——而Task.Supervisor是应用启动时就常驻的进程,不属于测试进程的管辖链路,所以输出会直接打印到控制台,无法被capture_io捕获。 - 额外细节:
IO.puts/1输出内容会自动追加尾部换行符\n,你原有断言的预期值没有带换行,就算捕获成功也会断言失败。
解决方案
方案1:生产代码侧显式绑定group leader(推荐)
修改handle_info/2中启动任务的逻辑,启动子任务时主动将任务进程的group leader设置为触发任务的GenServer进程,保证IO链路跟随调用方走:
# 先获取当前GenServer进程的pid caller = self() Task.Supervisor.start_child(Ex.TaskSupervisor, fn -> # 将子任务的group leader绑定为调用方进程 Process.group_leader(self(), caller) apply(module, function, args) end)
对应测试代码修改为用capture_io捕获,同时补全预期值的换行符:
test "use perform/0 function by default" do config = [%{cron: "* * * * * *", module: Hello}] assert capture_io(fn -> Worker.start_link(config) Process.sleep(1200) end) == "Perform!\n" end
这种改法不侵入测试逻辑,生产环境下IO链路也符合预期,不会出现输出路由错乱的问题。
方案2:测试侧临时修改Task.Supervisor的group leader(不修改生产代码时使用)
如果不想调整生产代码,可以在测试用例中临时将Ex.TaskSupervisor的group leader修改为当前测试进程,测试结束后恢复原值,避免影响其他用例:
test "use perform/0 function by default" do config = [%{cron: "* * * * * *", module: Hello}] task_sup_pid = Process.whereis(Ex.TaskSupervisor) original_gl = Process.group_leader(task_sup_pid, self()) try do output = capture_io(fn -> Process.group_leader(task_sup_pid, self()) Worker.start_link(config) Process.sleep(1200) end) assert output == "Perform!\n" after Process.group_leader(task_sup_pid, original_gl) end end
优化建议
固定用Process.sleep(1200)等待任务触发容易受测试环境负载影响出现偶发失败,可以在任务执行完成时给测试进程发一个确认信号,用assert_receive做等待,替代固定时长的sleep,提升测试稳定性。
内容的提问来源于stack exchange,提问作者Junaid Farooq
相关产品推荐
相关产品推荐

