在我们的代码库中,我们有很多涉及与数据库交互的测试(通过Postgrex)。我们有一些 shared ExUnit.CaseTemplate
s,它们的setup
钩子准备 Ecto 沙箱等,这很好用。
我遇到的问题是,当测试进程退出时,我们的测试产生的进程可能仍在与数据库通信,因此我们在日志中得到如下所示的错误:
09:56:51.517 [error] Postgrex.Protocol (#PID<0.2127.0>) disconnected: ** (DBConnection.ConnectionError) owner #PID<0.11626.0> exited
(这里的“所有者”PID 是测试进程,它当然在其设置中产生了数据库连接。)
我尝试过的事情:
在 shared
setup
挂钩中,添加on_exit/2
将清理数据库进程的处理程序。- 不解决问题。因为在测试进程终止后
on_exit
运行在一个单独的进程中,所以这里可能出现 5% 的竞争条件:Postgrex 可以在处理程序有机会完成其工作之前尝试与现已终止的测试进程通信。on_exit
- 不解决问题。因为在测试进程终止后
作为每个测试的最后一行,显式调用共享清理函数以等待数据库事务完成。
- 这行得通,但是当你有数百个测试时,它很麻烦,而且很容易忘记(在开发和 PR 审查中)。因此,它一直是我们测试不稳定的来源。
- 更糟糕的是,由于错误消息与测试异步发生,因此很难跟踪哪个测试缺少清理——在有问题的测试完成后的某个时间你会收到错误,但谁知道多久之后!
将ExUnit 源
test
中的宏复制并粘贴到我们的共享测试代码中,其中一项更改是将对我们的清理函数的调用附加到测试主体的末尾。- 这也有效,但它也很笨重,因为我们必须替换每次使用
test
withmy_test
或其他任何东西,并且我们将被困在维护 copypasta 中。 - 为了弹性,我们可以将整个测试主体包装在一个
try/catch
块中并调用清理函数,而不管测试过程如何退出。
- 这也有效,但它也很笨重,因为我们必须替换每次使用
我从Elixir 核心邮件列表中的探索中收集到,在之前on_exit/2
存在的情况下,曾经有一个teardown
在测试过程结束时同步运行的钩子。我真的很感激任何可以模仿这种功能的解决方案。
编辑添加:这是我们共享的示例ExUnit.CaseTemplate
:
defmodule PersistenceTestCase do
use ExUnit.CaseTemplate
setup tags do
# This line would be in test_helper.exs, but it breaks tests not using
# this test case that have implicit data-persistence side-effects
Ecto.Adapters.SQL.Sandbox.mode(App.Repo, :manual)
:ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()})
end
on_exit(fn ->
# Reset on exit to not interfere with tests not using this test case
Ecto.Adapters.SQL.Sandbox.mode(App.Repo, :auto)
end)
end
end