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

在Cabbage中隔离Gherkin场景:Elixir测试隔离方案问询

如何用Cabbage实现单个Gherkin场景的隔离

我刚好之前处理过类似的Cabbage场景隔离问题,你的痛点确实很典型——默认的setup太细(每个步骤都跑),setup_all又太粗(整个feature才跑一次)。其实Cabbage本身就提供了场景级的钩子,另外还有备选方案,给你详细说说:

方法1:用Cabbage原生的场景级钩子(最推荐)

Cabbage内置了before_scenario和after_scenario函数,它们会在每个场景的所有步骤执行前/后各运行一次,完全匹配你要的单个场景隔离需求。

举个实际的代码例子,假设你用Ecto做持久化:

defmodule UserManagementFeatureTest do
  use Cabbage.Feature, async: false # 持久化操作别开异步,避免竞争

  # 每个场景执行前重置持久化状态
  def before_scenario(_scenario_details, _tags) do
    # 这里可以是清空数据库表,或者用Ecto Sandbox启动事务
    MyApp.Repo.delete_all(MyApp.User)
    :ok
  end

  # 可选:场景结束后的清理(比如关闭外部连接)
  def after_scenario(_scenario_details, _tags) do
    :ok
  end

  # 你的Gherkin步骤定义...
  given ~r/系统中没有用户/, _vars, _state do
    assert MyApp.Repo.all(MyApp.User) == []
    {:ok, %{}}
  end

  when ~r/我创建一个名为"([^"]+)"的用户/, [name], _state do
    {:ok, user} = MyApp.create_user(%{name: name})
    {:ok, %{user: user}}
  end

  then ~r/系统中应该有一个名为"([^"]+)"的用户/, [name], %{user: user} do
    assert user.name == name
    assert MyApp.Repo.get_by(MyApp.User, name: name) != nil
  end
end

这个方法的好处是完全贴合Cabbage的设计,代码清晰,不需要额外的hack。

如果用Ecto的话,更高效的方式是用Sandbox事务隔离,代替删除数据:

def before_scenario(_scenario, _tags) do
  # 启动独立事务,场景结束后自动回滚
  {:ok, pid} = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: true)
  on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  :ok
end

这样比每次删除数据快很多,而且不会污染测试数据库。

方法2:结合ExUnit Tags的备选方案

如果因为某些限制没法用原生钩子,你可以给Gherkin场景加标签,然后在setup里判断是否是新场景的第一步。

先给场景加标签:

@isolated-scenario
Scenario: 创建用户并验证
  Given 系统中没有用户
  When 我创建名为"Bob"的用户
  Then 系统存在名为"Bob"的用户

@isolated-scenario
Scenario: 删除用户并验证
  Given 系统中有一个名为"Bob"的用户
  When 我删除名为"Bob"的用户
  Then 系统中没有名为"Bob"的用户

然后在测试模块里用进程存储跟踪当前场景:

defmodule UserManagementFeatureTest do
  use Cabbage.Feature, async: false

  setup %{scenario: scenario} do
    # 用场景ID判断是否是新场景
    current_scenario_id = Process.get(:active_scenario_id)
    if current_scenario_id != scenario.id do
      # 重置持久化
      MyApp.Repo.delete_all(MyApp.User)
      Process.put(:active_scenario_id, scenario.id)
    end
    :ok
  end

  # 步骤定义...
end

这个方法不如原生钩子优雅,但在特殊场景下可以应急。

关键注意事项

  • 一定要把async: false加上:持久化操作是有状态的,异步测试会导致多个场景同时修改数据,隔离完全失效。
  • 优先用原生钩子:Cabbage的before_scenario是官方设计用来处理场景级初始化的,比自己造轮子靠谱得多。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 06:38:09