异步作业与测试:Oban与Elixir测试框架的实践
在开发应用程序时,异步作业和测试是两个至关重要的方面。异步作业可以提高应用程序的性能和响应能力,而测试则能确保应用程序的正确性和稳定性。本文将深入探讨Oban这个强大的异步作业处理库,并介绍Elixir内置的测试框架。
1. Oban vs. Sidekiq
在Ruby开发中,Sidekiq库已经存在了一段时间,开发者们对它非常熟悉。它在生产环境中极其稳定,每秒能够处理大量的作业。Sidekiq使用Redis来存储和处理作业,而Oban则基于Postgres数据库运行。尽管Redis和Postgres的操作方式有所不同,但大规模使用Oban并不困难。Elixir社区的实践证明,Oban是一个稳定的作业系统,能够处理大量的作业。此外,它还具备事务处理能力,并且无需运行Redis服务器,这些优势使其成为一个极具吸引力的作业系统。
1.1 实现Oban Worker
在我们的SMS应用中实现Oban Worker,需要以下步骤:
1.1.1 添加Oban到SMS应用
-
步骤1
:在
mix.exs中添加Oban依赖:
{:oban, "~> 2.16"},
-
步骤2
:运行
mix deps.get将其引入项目。 - 步骤3 :创建新的迁移文件:
$ mix ecto.gen.migration add_oban_jobs_table
- 步骤4 :在迁移文件中添加以下代码:
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
-
步骤5
:运行
mix ecto.migrate执行迁移。 -
步骤6
:在
config.exs中添加Oban队列配置:
config :phone_app, Oban,
repo: PhoneApp.Repo,
plugins: [
# 1 hour
{Oban.Plugins.Pruner, max_age: 60 * 60}
],
queues: [default: 10]
-
步骤7
:更新
test.exs配置:
config :phone_app, Oban, testing: :manual
-
步骤8
:在
lib/phone_app/application.ex的start函数中添加Oban Supervisor:
{Finch, name: PhoneApp.Finch},
{Oban, Application.fetch_env!(:phone_app, Oban)},
PhoneAppWeb.Endpoint
- 步骤9 :验证配置是否正确:
$ iex -S mix
iex> Oban.config()
%Oban.Config{
...
}
1.1.2 编写Oban Worker
我们的SMS应用在发送消息时存在问题,消息不会立即送达,因此需要跟踪消息的状态。当发送消息时,我们将使用异步作业来检查消息的状态,直到其最终确定。以下是具体实现:
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
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
end
同时,需要添加
SmsMessageStore.get_sms_message!/1
函数:
def get_sms_message!(id) do
Repo.get!(SmsMessage, id)
end
1.1.3 短信创建后入队作业
我们的作业应该在创建具有特定属性(
status = queued
且
direction = outgoing
)的SMS消息时入队。在
lib/phone_app/conversations/conversations.ex
文件中,修改
create_sms_message/1
函数:
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
1.2 运行效果
启动
phone_app
应用和
mock_server
应用,访问
http://localhost:4004/messages
创建新的SMS消息,然后访问
http://localhost:4005/
查看消息。最初,SMS应用会显示消息处于排队状态,短时间后刷新页面,会发现“queued”状态消失,系统正确识别消息已送达。
1.3 Oban的更多特性
Oban不仅易于上手,还提供了许多强大的功能,部分功能是开源的,还有付费版本提供更多高级功能。
1.3.1 遥测(开源)
可观测性是一个优秀作业系统的基本要求。Oban使用标准化的Telemetry库,让你可以选择要监控的事件。以下是一个示例:
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
1.3.2 CRON作业(开源)
Oban提供了CRON插件,方便按重复计划运行作业。以下是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},
]}
]
1.3.3 恢复卡住的作业(开源)
作业只有在完全处理或失败足够多次后才会从系统中移除。但在应用程序重启或遇到外部问题时,作业可能会卡住。Oban的Lifeline插件可以恢复这些卡住的作业,这是免费版本的Sidekiq所不具备的优势。
1.3.4 网页界面(付费)
Oban提供付费的网页界面,类似于Sidekiq的专业版。该界面虽不开源,但费用支持Oban的开发,会定期添加新功能并保持界面的更新。
1.3.5 专业功能(付费)
付费版本的Oban提供了一系列强大的专业功能,如更强大的执行引擎、批量作业支持、严格有序的作业执行和扇入扇出工作流等。
1.4 总结
在开发应用程序时,异步作业是必不可少的。Elixir提供了构建高度并行系统的基础,但建议使用像Oban这样的现成库,它提供了受控并发、事务处理和审计等功能。Oban易于上手,同时提供了许多高级功能,能帮助你的应用程序不断发展。
以下是Oban配置和使用的流程图:
graph LR
A[添加Oban依赖] --> B[创建迁移文件]
B --> C[执行迁移]
C --> D[配置Oban队列]
D --> E[更新测试配置]
E --> F[添加Oban Supervisor]
F --> G[验证配置]
G --> H[编写Oban Worker]
H --> I[短信创建后入队作业]
1.5 测试Elixir
测试对于确保应用程序的正确性和可预测性至关重要。Elixir拥有优秀的内置测试框架ExUnit,以下是使用ExUnit进行测试的详细介绍。
1.5.1 创建第一个测试
在开始测试之前,我们先比较一下Ruby和Elixir的测试理念。Ruby的测试历史上主要由RSpec主导,但近年来Minitest逐渐复兴。Minitest提供最少的断言,使测试保持简单;而RSpec则提供更全面的断言库和多种数据设置选项。Elixir的测试框架ExUnit随语言一起提供,无需额外安装。它类似于Minitest,使用简单的断言和最少的测试用例设置选项,这种简约的方法被证明非常强大,易于学习,并且能让你编写简洁、富有表现力的测试。
1.5.2 测试NewMessage Changeset
我们的第一个测试是针对Ecto changeset的。这个测试不涉及数据库或网络请求,只需要调用一个函数并检查其输出。
-
步骤1
:验证测试环境是否正确设置,运行
mix test确保现有测试通过:
$ mix test
.....
Finished in 0.08 seconds (0.04s async, 0.04s sync)
5 tests, 0 failures
如果遇到错误,可能是数据库设置问题,可运行
MIX_ENV=test mix do ecto.drop, ecto.create, ecto.migrate
重置测试数据库。
-
步骤2
:创建测试文件
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
注意文件名必须使用
.exs
扩展名。
- 步骤3 :添加测试用例,测试必填字段、自动添加国家代码和电话号码验证:
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
运行
mix test
确保新测试通过。
1.5.3 深入了解变更集测试
我们使用
describe/2
宏对测试进行分组,使用
test/2
宏定义单个测试用例。在测试用例中,我们可以直接访问应用程序的模块和函数,使用
assert/1
函数进行断言。
assert/1
函数既可以进行简单的相等断言,也可以使用模式匹配。模式匹配断言的强大之处在于,当模式不匹配时,ExUnit会以易读的、彩色编码的格式显示差异,让你清楚地知道左右两侧的不同之处。
以下是测试流程的表格总结:
| 步骤 | 操作 | 命令/代码 |
| ---- | ---- | ---- |
| 1 | 验证测试环境 |
mix test
|
| 2 | 创建测试文件 |
test/phone_app/conversations/schema/new_message_test.exs
|
| 3 | 添加测试用例 | 见上述代码 |
| 4 | 运行测试 |
mix test
|
1.6 测试原则
并非所有内容都需要测试,需要在测试价值、工作量和业务价值之间找到平衡。通常,不建议测试框架本身,而是测试应用程序实现的功能,如必填字段和验证逻辑。当不确定是否需要测试时,最好进行测试,但不要让测试成为完成功能的障碍。
通过以上内容,我们了解了Oban的使用和Elixir的测试方法,这些知识将帮助你构建更稳定、可靠的应用程序。
2. 测试不同场景
2.1 测试Ecto查询
在应用中,Ecto查询是常见的操作。为了测试Ecto查询,我们需要创建一个测试环境,模拟数据库操作。以下是一个简单的示例,假设我们有一个
User
模块,并且想要测试一个查询用户的函数。
首先,创建测试文件
test/phone_app/users/query/user_query_test.exs
:
defmodule PhoneApp.Users.Query.UserQueryTest do
use ExUnit.Case, async: true
alias PhoneApp.Users.Query.UserQuery
alias PhoneApp.Repo
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
:ok
end
test "get user by id" do
user = Repo.insert!(%PhoneApp.Users.Schema.User{name: "Test User", email: "test@example.com"})
result = UserQuery.get_user_by_id(user.id)
assert result.id == user.id
end
end
在这个测试中,我们使用了
Ecto.Adapters.SQL.Sandbox
来隔离测试环境,确保每个测试用例都有独立的数据库状态。
setup
函数会在每个测试用例执行前运行,用于设置数据库沙箱。
2.2 测试外部API请求
当应用需要与外部API进行交互时,我们需要模拟这些请求,以确保测试的独立性和可重复性。可以使用
Mox
库来模拟外部API客户端。
首先,添加
Mox
依赖到
mix.exs
:
{:mox, "~> 1.0", only: :test}
然后运行
mix deps.get
安装依赖。
创建一个模拟模块
test/phone_app/mocks/twilio_mock.exs
:
defmodule PhoneApp.Mocks.TwilioMock do
use Mox
def expect_get_sms_message!(message) do
expect(:get_sms_message!, fn ^message ->
%{body: %{"status" => "delivered"}}
end)
end
end
在测试文件中使用模拟模块:
defmodule PhoneApp.Conversations.Worker.StatusWorkerTest do
use ExUnit.Case, async: true
import Mox
alias PhoneApp.Conversations.Worker.StatusWorker
alias PhoneApp.Conversations.Schema.SmsMessage
setup :verify_on_exit!
test "perform job when message is delivered" do
message = %SmsMessage{id: 1, message_sid: "123"}
PhoneApp.Mocks.TwilioMock.expect_get_sms_message!(message)
result = StatusWorker.perform(%Oban.Job{args: %{"id" => message.id}})
assert {:ok, _} = result
end
end
在这个测试中,我们使用
Mox
来模拟
PhoneApp.Twilio.get_sms_message!
函数的返回值,从而避免实际的API请求。
2.3 测试Phoenix请求
Phoenix是一个流行的Elixir Web框架,测试Phoenix请求可以确保路由和控制器的正确性。
创建测试文件
test/phone_app_web/controllers/user_controller_test.exs
:
defmodule PhoneAppWeb.UserControllerTest do
use PhoneAppWeb.ConnCase
test "GET /users", %{conn: conn} do
conn = get(conn, Routes.user_path(conn, :index))
assert html_response(conn, 200) =~ "Users"
end
end
在这个测试中,我们使用
PhoneAppWeb.ConnCase
来创建一个模拟的HTTP连接,然后发送一个GET请求到
/users
路由,并验证响应状态码和响应内容。
3. 测试策略总结
3.1 测试类型总结
| 测试类型 | 描述 | 示例 |
|---|---|---|
| 单元测试 | 测试单个函数或模块的功能 | 测试Ecto changeset、Ecto查询 |
| 集成测试 | 测试多个模块之间的交互 | 测试Phoenix请求、外部API请求 |
| 端到端测试 | 模拟用户在应用中的实际操作 | 使用工具如Wallaby进行浏览器自动化测试 |
3.2 测试流程总结
graph LR
A[编写测试用例] --> B[设置测试环境]
B --> C[执行测试]
C --> D[验证结果]
D --> E{测试是否通过}
E -- 是 --> F[完成测试]
E -- 否 --> G[调试修复]
G --> A
4. 结论
在开发过程中,异步作业和测试都是不可或缺的环节。Oban为我们提供了一个强大而稳定的异步作业处理方案,它基于Postgres数据库,支持事务处理,并且提供了丰富的开源和付费功能。同时,Elixir的内置测试框架ExUnit让我们能够方便地编写各种类型的测试,确保应用程序的正确性和可维护性。
通过合理运用Oban和ExUnit,我们可以构建出高效、稳定、可靠的应用程序。在实际开发中,我们应该根据应用的需求和复杂度,选择合适的测试策略和工具,不断优化测试流程,提高开发效率和软件质量。
希望本文介绍的内容能够帮助你更好地理解和应用Oban和ExUnit,让你的Elixir开发之旅更加顺利。
超级会员免费看
84

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



