15、深入探索 Elixir 异步任务与测试:Oban 与 ExUnit 的实战应用

深入探索 Elixir 异步任务与测试:Oban 与 ExUnit 的实战应用

在构建应用程序时,异步任务的处理和测试是至关重要的环节。本文将详细介绍如何在 Elixir 应用中集成 Oban 来处理异步任务,并使用 ExUnit 进行测试。

1. 集成 Oban 到 SMS 应用

要在应用中使用 Oban,需要完成以下几个关键步骤:
1. 添加依赖 :在 mix.exs 文件中添加 Oban 依赖:

# phone_app/mix.exs
{:oban, "~> 2.16"},

然后运行 mix deps.get 将其引入项目。
2. 数据库迁移 :创建新的迁移文件:

$ mix ecto.gen.migration add_oban_jobs_table

在迁移文件中添加以下代码:

# phone_app/priv/repo/migrations/20231112032011_add_oban_jobs_table.exs
defmodule PhoneApp.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def up do
    Oban.Migration.up(version: 11)
  end

  def down do
    Oban.Migration.down(version: 1)
  end
end

运行 mix ecto.migrate 执行迁移。
3. 配置 Oban :在 config.exs 中添加 Oban 配置:

# phone_app/config/config.exs
config :phone_app, Oban,
  repo: PhoneApp.Repo,
  plugins: [
    # 1 hour
    {Oban.Plugins.Pruner, max_age: 60 * 60}
  ],
  queues: [default: 10]

test.exs 中更新配置:

# phone_app/config/test.exs
config :phone_app, Oban, testing: :manual
  1. 添加到应用进程列表 :在 lib/phone_app/application.ex 中更新 start 函数:
# phone_app/lib/phone_app/application.ex
{Finch, name: PhoneApp.Finch},
{Oban, Application.fetch_env!(:phone_app, Oban)},
PhoneAppWeb.Endpoint

可以通过以下命令验证配置是否生效:

$ iex -S mix
iex> Oban.config()
2. 编写 Oban 工作器

在 SMS 应用中,消息发送可能会遇到延迟或失败的情况,因此需要跟踪消息状态。以下是创建工作器的步骤:
1. 创建工作器模块和入队函数

# phone_app/lib/phone_app/conversations/worker/status_worker.ex
defmodule PhoneApp.Conversations.Worker.StatusWorker do
  use Oban.Worker

  alias PhoneApp.Conversations.Schema.SmsMessage

  def enqueue(%SmsMessage{} = message) do
    %{"id" => message.id}
    |> new()
    |> Oban.insert()
  end
end
  1. 实现 perform/1 函数
# phone_app/lib/phone_app/conversations/worker/status_worker.ex
alias PhoneApp.Conversations.Query.SmsMessageStore

def perform(%Oban.Job{args: %{"id" => message_id}}) do
  message = SmsMessageStore.get_sms_message!(message_id)
  %{body: resp} = PhoneApp.Twilio.get_sms_message!(message)

  case resp["status"] do
    "queued" ->
      {:error, "Message not ready"}

    status ->
      PhoneApp.Conversations.update_sms_message(
        message.message_sid,
        %{status: status}
      )
  end
end
  1. 添加 get_sms_message!/1 函数
# phone_app/lib/phone_app/conversations/query/sms_message_store.ex
def get_sms_message!(id) do
  Repo.get!(SmsMessage, id)
end
  1. 集成到 SMS 创建流程 :在 lib/phone_app/conversations/conversations.ex 中更新 create_sms_message 函数:
# phone_app/lib/phone_app/conversations/conversations.ex
def create_sms_message(params) do
  PhoneApp.Repo.transaction(fn ->
    with {:ok, message} <- Query.SmsMessageStore.create_sms_message(params),
         {:ok, _} <- maybe_enqueue_status_worker(message) do
      message
    else
      {:error, cs} -> PhoneApp.Repo.rollback(cs)
    end
  end)
end

defp maybe_enqueue_status_worker(message) do
  case message do
    %{direction: :outgoing, status: "queued"} ->
      PhoneApp.Conversations.Worker.StatusWorker.enqueue(message)

    _ ->
      {:ok, :skipped}
  end
end
3. 工作器实战

启动 phone_app 应用和 mock_server 应用:

$ mix phx.server

访问 http://localhost:4004/messages 创建新的 SMS 消息,然后访问 http://localhost:4005/ 查看消息。一段时间后,消息状态将从“排队中”更新为“已送达”。

4. Oban 的高级特性

Oban 提供了许多高级特性,包括:
| 特性 | 描述 |
| ---- | ---- |
| 遥测(Telemetry) | 使用标准化的 Telemetry 库监控作业事件,可用于更新统计系统。 |
| CRON 作业 | 提供 CRON 插件,方便按重复时间表运行作业。 |
| 恢复卡住的作业 | 提供 Lifeline 插件,可恢复因应用重启或外部问题而卡住的作业。 |
| 网页界面(付费) | 提供付费的网页界面,方便管理作业。 |
| 专业特性(付费) | 包括更强大的执行引擎、批量作业支持等。 |

以下是遥测和 CRON 作业的示例代码:

# 遥测示例
defmodule Super.Application do
  use Application

  def start(_type, _args) do
    events = [
      [:oban, :job, :start],
      [:oban, :job, :stop],
      [:oban, :job, :exception]
    ]

    :telemetry.attach_many("oban-logger", events,
                           &Super.ObanLogger.handle_event/4, [])
    # rest of application setup
  end
end

defmodule Super.ObanLogger do
  require Logger

  def handle_event([:oban, :job, :start], _measure, meta, _) do
    Logger.info("[Oban] started #{meta.worker}")
  end

  def handle_event([:oban, :job, event], measure, meta, _) do
    ms = ceil(measure.duration / 1_000_000)
    Logger.info("[Oban] #{event} #{meta.worker} ran in #{ms}ms")
  end
end

# CRON 作业示例
config :my_app, Oban,
  plugins: [
    {Oban.Plugins.Cron,
     crontab: [
       {"* * * * *", MyApp.MinuteWorker},
       {"0 0 * * *", MyApp.DailyWorker, max_attempts: 1},
       {"0 12 * * MON", MyApp.MondayWorker, queue: :scheduled},
     ]}
  ]
5. 编写 Elixir 测试

测试对于确保应用程序的正确性和可预测性至关重要。Elixir 提供了内置的测试框架 ExUnit
1. 测试 Ecto 变更集 :首先验证测试环境是否正确设置,运行 mix test

$ mix test

如果遇到数据库问题,运行以下命令重置测试数据库:

$ MIX_ENV=test mix do ecto.drop, ecto.create, ecto.migrate

创建测试文件:

# phone_app/test/phone_app/conversations/schema/new_message_test.exs
defmodule PhoneApp.Conversations.Schema.NewMessageTest do
  use ExUnit.Case, async: true

  alias PhoneApp.Conversations.Schema.NewMessage
end

添加测试用例:

# phone_app/test/phone_app/conversations/schema/new_message_test.exs
describe "changeset/1" do
  test "fields are required" do
    cs = NewMessage.changeset(%{})

    assert [
      to: {"can't be blank", _}, body: {"can't be blank", _}
    ] = cs.errors
  end

  test "the country code is added if not present" do
    assert %{ 
      errors: [],
      changes: %{to: "+1 5005550006"}
    } = NewMessage.changeset(%{"body" => "test", "to" => "5005550006"})
  end

  test "the phone number is validated" do
    assert %{
      errors: [to: {"is an invalid phone number", _}]
    } = NewMessage.changeset(%{"body" => "test", "to" => "+1 111-222-3333"})
  end
end
  1. 变更集测试解析
    • describe/2 宏用于分组和命名测试。
    • test/2 宏用于定义单个测试用例。
    • assert/1 函数用于断言,支持简单的相等断言和模式匹配。

以下是测试流程的 mermaid 流程图:

graph TD;
    A[验证测试环境] --> B[运行 mix test];
    B --> C{测试是否通过};
    C -- 是 --> D[创建测试文件];
    C -- 否 --> E[重置测试数据库];
    E --> B;
    D --> F[添加测试用例];
    F --> G[运行 mix test 验证];
6. 测试要点

在决定测试内容时,需要在测试价值、工作量和业务价值之间取得平衡。通常不需要测试框架本身,而应关注应用程序实现的功能,如必需字段和验证逻辑。当不确定是否需要测试时,建议进行测试,但不要让测试成为完成功能的障碍。

通过以上步骤,你可以在 Elixir 应用中成功集成 Oban 处理异步任务,并使用 ExUnit 进行有效的测试。

深入探索 Elixir 异步任务与测试:Oban 与 ExUnit 的实战应用

7. 测试框架对比:Ruby 与 Elixir

在测试方面,Ruby 和 Elixir 有着不同的测试框架和理念。

语言 主要测试框架 特点
Ruby RSpec、Minitest RSpec 提供全面的断言库和丰富的数据设置选项;Minitest 提供最小化的断言集,使测试保持简单。
Elixir ExUnit 是 Elixir 内置的测试框架,使用简单的断言和最少的测试用例设置选项,易于学习,能满足编写简洁、表达性强的测试需求。
8. 测试用例的结构与执行

在编写测试用例时,了解其结构和执行方式是很重要的。以下是一个可视化的流程:

graph LR;
    A[describe 宏分组测试] --> B[test 宏定义单个测试用例];
    B --> C[在测试用例中使用 assert 断言];
    C --> D{断言是否通过};
    D -- 是 --> E[测试通过];
    D -- 否 --> F[测试失败];
  • describe/2 宏 :用于将相关的测试用例分组,通常基于被测试的函数名进行命名,这有助于组织和管理测试代码。
  • test/2 宏 :每个 test 块内的代码作为一个独立的测试用例运行。在测试用例中,可以直接使用应用程序中的模块和函数,因为 ExUnit 会自动包含应用程序的所有内容。
  • assert/1 函数 :这是测试中最常用的函数,它支持简单的相等断言,也可以使用模式匹配进行断言。当模式不匹配时,ExUnit 会以易读的、彩色编码的格式显示差异,方便定位问题。
9. 不同类型测试的注意事项

在实际测试中,不同类型的测试有不同的注意事项:
- Ecto 变更集测试 :这类测试不涉及数据库或网络请求,只需调用函数并检查输出。确保测试环境正确设置,如果遇到数据库问题,及时重置测试数据库。
- 其他类型测试(如 Ecto 查询、外部 API 请求、Phoenix 请求、Oban 作业等) :这些测试可能会涉及更复杂的依赖和流程,需要根据具体情况进行处理。例如,测试外部 API 请求时,可能需要模拟 API 响应;测试 Phoenix 请求时,需要考虑路由和控制器的交互。

以下是一个简单的测试外部 API 请求的示例思路:

# 假设我们有一个函数用于调用外部 API
defmodule MyApp.ApiClient do
  def get_data do
    # 实际的 API 请求代码
  end
end

# 测试代码
defmodule MyApp.ApiClientTest do
  use ExUnit.Case, async: true

  test "get_data returns valid data" do
    # 模拟 API 响应
    mock_response = %{status: 200, body: %{data: "test data"}}
    # 这里可以使用一些模拟库来替换实际的 API 请求
    # 例如使用 Mox 库
    assert %{data: _} = MyApp.ApiClient.get_data()
  end
end
10. 异步任务与测试的综合应用

在实际应用中,异步任务和测试是紧密结合的。例如,在使用 Oban 处理异步任务时,需要对工作器进行测试,以确保任务能正确执行。
- 测试工作器 :可以编写测试用例来验证工作器的 perform/1 函数是否按预期工作。例如,模拟消息状态,检查工作器是否能正确更新消息状态。

defmodule PhoneApp.Conversations.Worker.StatusWorkerTest do
  use ExUnit.Case, async: true
  alias PhoneApp.Conversations.Worker.StatusWorker

  test "perform updates message status" do
    # 模拟消息
    message = %{id: 1, message_sid: "123", status: "sending"}
    # 模拟 Twilio API 响应
    resp = %{"status" => "delivered"}
    # 这里可以使用一些模拟库来替换实际的 API 请求
    assert {:ok, _} = StatusWorker.perform(%{args: %{"id" => message.id}})
    # 检查消息状态是否更新
    # 可以通过查询数据库或其他方式验证
  end
end
  • 结合事务测试 :在集成工作器到业务流程时,使用事务可以确保数据和任务的一致性。在测试中,也需要考虑事务的影响,确保在事务回滚时,任务不会被错误执行。
11. 总结与展望

通过本文的介绍,我们详细了解了如何在 Elixir 应用中集成 Oban 处理异步任务,以及如何使用 ExUnit 进行测试。Oban 提供了强大的异步任务处理能力,包括多种高级特性,能满足不同场景的需求;ExUnit 则为我们提供了简单而有效的测试手段,帮助我们确保应用程序的正确性和可预测性。

在未来的开发中,可以进一步探索 Oban 和 ExUnit 的更多功能,例如自定义 Oban 的插件和重试策略,编写更复杂的测试用例来覆盖更多的业务场景。同时,要始终牢记在测试中取得测试价值、工作量和业务价值的平衡,让测试成为开发过程中的有力保障。

通过不断实践和学习,你将能够熟练运用 Oban 和 ExUnit,构建出高质量、可靠的 Elixir 应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值