深入探索 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
-
添加到应用进程列表
:在
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
-
实现
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
-
添加
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
-
集成到 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
-
变更集测试解析
:
-
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 应用程序。
超级会员免费看

被折叠的 条评论
为什么被折叠?



