17、Elixir 应用测试指南

Elixir 应用测试指南

1. 测试 Ecto 查询

测试使用数据库查询的代码很简单,借助 Phoenix 生成器提供的特殊测试用例,其中包含事务性测试用例,每次测试后测试数据库会自动清理。

1.1 测试 ContactStore 模块

我们选择测试 ContactStore 模块,因为它包含数据读写功能。以下是具体的测试代码:

# phone_app/test/phone_app/conversations/query/contact_store_test.exs
defmodule PhoneApp.Conversations.Query.ContactStoreTest do
  use PhoneApp.DataCase, async: true

  alias PhoneApp.Conversations.Query.ContactStore

  describe "upsert_contact/1" do
    @incoming %{
      from: "111-222-3333",
      to: "999-888-7777",
      direction: :incoming
    }

    @outgoing %{@incoming | direction: :outgoing}

    test "a new contact is created, based on direction" do
      assert {:ok, contact} = ContactStore.upsert_contact(@incoming)
      assert contact.id
      assert contact.phone_number == "111-222-3333"

      assert {:ok, contact2} = ContactStore.upsert_contact(@outgoing)
      assert contact2.id
      assert contact2.id != contact.id
      assert contact2.phone_number == "999-888-7777"
    end

    test "a contact with the same phone number is updated" do
      assert {:ok, contact} = ContactStore.upsert_contact(@incoming)
      assert {:ok, contact2} = ContactStore.upsert_contact(@incoming)

      assert contact2.updated_at != contact.updated_at
      assert Map.delete(contact2, :updated_at) ==
             Map.delete(contact, :updated_at)
    end
  end

  describe "get_contact!/1" do
    test "no contact raises an error" do
      assert_raise(Ecto.NoResultsError, fn ->
        ContactStore.get_contact!(0)
      end)
    end

    test "a contact is returned" do
      assert {:ok, contact} = ContactStore.upsert_contact(@incoming)
      assert ContactStore.get_contact!(contact.id) == contact
    end
  end
end
  • use PhoneApp.DataCase :该模块由 Phoenix 生成器创建,位于 test/support/data_case.ex ,它基于 ExUnit.Case ,并设置了 PhoneApp.Repo ,以便在测试模块中使用。
  • 模块属性:如 @incoming @outgoing ,可简化测试代码。
  • 断言类型:测试模块中混合使用了模式断言(如 assert {:ok, contact} = ContactStore.upsert_contact(@incoming) )和严格相等断言(如 assert contact.phone_number == "111-222-3333" ),还使用了 assert_raise/2 断言来验证特定错误是否抛出。

1.2 修复原代码问题

这些测试揭示了原 Repo.insert/2 函数的一个 bug,修复方法是在插入选项中添加 returning: true 。若测试出现错误,需确保在 ContactStore.upsert_contact/1 中使用该选项。

2. 测试外部 API 请求

对于如何测试外部请求,有多种观点。在 Elixir 中,可选择模拟函数或模拟 HTTP 请求,这里推荐使用 Bypass 库进行 HTTP 请求模拟。

2.1 添加 Bypass 库

  • mix.exs 文件中添加依赖:
# phone_app/mix.exs
{:bypass, "~> 2.1"}
  • 运行 mix deps.get 拉取依赖。
  • test.exs 中添加配置:
# phone_app/config/test.exs
config :phone_app, :twilio,
  key_sid: "mock-key-sid",
  key_secret: "mock-key",
  account_sid: "mock-account",
  number: "+19998887777",
  base_url: "http://localhost:4005/2010-04-01"
  • 创建 JSON 文件 test/support/fixtures/success.json
{
  "account_sid": "account_sid",
  "sid": "sid",
  "body": "body",
  "from": "+11112223333",
  "to": "+19998887777",
  "status": "sent"
}

2.2 测试 API 请求

测试 Conversations.send_sms_message/1 函数,确保 HTTP 请求正常工作且函数处理正确。以下是测试代码:

# phone_app/test/phone_app/conversations/conversations_test.exs
defmodule PhoneApp.ConversationsTest do
  use PhoneApp.DataCase, async: true

  alias PhoneApp.Conversations
  alias PhoneApp.Conversations.Schema.NewMessage

  describe "send_sms_message/1" do
    test "successful request creates an SMS message" do
      bypass = Bypass.open()
      Process.put(:twilio_base_url, "http://localhost:#{bypass.port}")

      resp = Jason.decode!(File.read!("test/support/fixtures/success.json"))

      Bypass.expect_once(
        bypass,
        "POST",
        "/Accounts/mock-account/Messages.json",
        fn conn ->
          conn
          |> Plug.Conn.put_resp_header("Content-Type", "application/json")
          |> Plug.Conn.resp(201, Jason.encode!(resp))
        end
      )

      assert {:ok, message} = Conversations.send_sms_message(%NewMessage{})
      assert message.from == resp["from"]
      assert message.to == resp["to"]
      assert message.body == resp["body"]
      assert message.message_sid == resp["sid"]
      assert message.account_sid == resp["account_sid"]
      assert message.status == resp["status"]
      assert message.direction == :outgoing
    end

    test "a failed request returns an error" do
      bypass = Bypass.open()
      Process.put(:twilio_base_url, "http://localhost:#{bypass.port}")

      Bypass.expect_once(
        bypass,
        "POST",
        "/Accounts/mock-account/Messages.json",
        fn conn ->
          Plug.Conn.resp(conn, 500, "")
        end
      )

      assert Conversations.send_sms_message(%NewMessage{}) ==
               {:error, "Failed to send message"}
    end
  end
end
  • Bypass.open/0 :创建测试服务器端点。
  • Process.put(:twilio_base_url, ...) :将 Bypass URL 放入进程字典,使 API 客户端向 Bypass 发送请求。
  • Bypass.expect_once/4 :指定预期的 HTTP 请求细节,并提供返回 HTTP 响应的函数。

2.3 测试流程总结

graph TD;
    A[添加 Bypass 依赖] --> B[配置测试环境];
    B --> C[创建测试文件];
    C --> D[编写测试代码];
    D --> E[运行测试];

3. 测试 Phoenix 请求

测试 Phoenix 端点比测试纯函数或 Ecto 查询更复杂,但 Phoenix 提供了一些辅助函数简化测试。

3.1 创建辅助工厂

为避免重复创建测试数据,可使用测试工厂。以下是创建 SMS 消息的工厂代码:

# phone_app/test/support/factory/sms_message.ex
defmodule Test.Factory.SmsMessageFactory do
  def params(overrides \\ %{}) do
    %{
      from: "+11112223333",
      to: "+19998887777",
      direction: :outgoing,
      message_sid: Ecto.UUID.generate(),
      account_sid: "account_sid",
      body: "body",
      status: "queued"
    }
    |> Map.merge(overrides)
  end

  def create(overrides \\ %{}) do
    {:ok, message} =
      overrides
      |> params()
      |> PhoneApp.Conversations.create_sms_message()
    message
  end
end

3.2 设置测试模块

创建测试模块 MessageControllerTest

# phone_app/test/phone_app_web/controllers/message_controller_test.exs
defmodule PhoneAppWeb.MessageControllerTest do
  use PhoneAppWeb.ConnCase, async: true
  alias Test.Factory.SmsMessageFactory
end
  • use PhoneAppWeb.ConnCase :该模块由 Phoenix 创建,设置了 conn 依赖以及与 DataCase 相同的依赖。

3.3 测试重定向

测试 GET /messages 请求的重定向逻辑:

describe "GET /messages" do
  test "empty messages redirects to new message", %{conn: conn} do
    conn = get(conn, ~p"/messages")
    assert redirected_to(conn, 302) == "/messages/new"
  end

  test "redirects to latest messages", %{conn: conn} do
    _m1 = SmsMessageFactory.create(%{to: "111-222-3333", body: "Test 1"})
    m2 = SmsMessageFactory.create(%{to: "211-222-3333", body: "Test 2"})
    conn = get(conn, ~p"/messages")
    assert redirected_to(conn, 302) == "/messages/#{m2.contact_id}"
  end
end

3.4 测试 HTML 响应

测试 GET /messages/new 请求的 HTML 响应:

describe "GET /messages/new" do
  test "a message form is rendered", %{conn: conn} do
    conn = get(conn, ~p"/messages/new")
    assert html = html_response(conn, 200)
    assert html =~ ~S(<form action="/messages/new" method="post")
    assert html =~ "Send a message..."
    assert html =~ "To (Phone Number)"
  end
end

3.5 测试 POST 请求

测试 POST /messages/new 请求,由于该端点会向 Twilio 发送出站 HTTP 请求,需使用 Bypass 进行测试:

alias PhoneApp.Conversations.Schema.SmsMessage

describe "POST /messages/new" do
  test "invalid params is rejected", %{conn: conn} do
    conn = post(conn, ~p"/messages/new", %{})
    assert html_response(conn, 200) =~
             Plug.HTML.html_escape("can't be blank")
  end

  test "valid params creates a message", %{conn: conn} do
    bypass = Bypass.open()
    Process.put(:twilio_base_url, "http://localhost:#{bypass.port}")

    Bypass.expect_once(
      bypass,
      "POST",
      "/Accounts/mock-account/Messages.json",
      fn conn ->
        conn
        |> put_resp_header("Content-Type", "application/json")
        |> resp(201, File.read!("test/support/fixtures/success.json"))
      end
    )

    params = %{message: %{to: "+1111-222-3333", body: "Test"}}
    conn = post(conn, ~p"/messages/new", params)
    assert redirected_to(conn, 302) == "/messages"
    assert PhoneApp.Repo.aggregate(SmsMessage, :count) == 1
  end
end
  • 注意使用 Plug.HTML.html_escape/1 处理 HTML 断言,避免因 HTML 实体转义导致测试失败。

3.6 测试类型总结

测试类型 请求方法 测试内容
重定向测试 GET /messages 验证请求是否根据会话情况正确重定向
HTML 响应测试 GET /messages/new 验证返回的 HTML 内容是否包含关键元素
POST 请求测试 POST /messages/new 验证有效和无效参数的处理情况

4. 测试 Oban 作业

在使用 Oban 时,通常需要测试两个方面:作业是否正确入队以及作业是否按预期工作。Oban 为这两种情况都提供了测试辅助函数。

4.1 测试作业入队

当在 PhoneApp.Conversations 模块中创建 SMS 消息时,会根据消息是否为出站消息有条件地创建 Oban 作业。我们使用 Oban.Testing 模块来验证 create_sms_message/1 函数的正确性。以下是添加到现有 ConversationsTest 模块的代码:

# phone_app/test/phone_app/conversations/conversations_test.exs
use Oban.Testing, repo: Repo
alias PhoneApp.Conversations.Worker.StatusWorker

describe "create_sms_message/1" do
  test "a valid SMS message is created" do
    params = Test.Factory.SmsMessageFactory.params()
    assert {:ok, msg} = Conversations.create_sms_message(params)
    assert_enqueued(worker: StatusWorker, args: %{"id" => msg.id})
  end

  test "incoming SMS message doesn't enqueue a worker" do
    params = Test.Factory.SmsMessageFactory.params(%{direction: :incoming})
    assert {:ok, _msg} = Conversations.create_sms_message(params)
    refute_enqueued(worker: StatusWorker)
  end

  test "an invalid message returns an error" do
    params = Test.Factory.SmsMessageFactory.params(%{message_sid: ""})
    assert {:error, _} = Conversations.create_sms_message(params)
    refute_enqueued(worker: StatusWorker)
  end
end
  • use Oban.Testing, repo: Repo :引入 Oban 测试功能。
  • assert_enqueued refute_enqueued :分别用于验证作业是否入队和未入队。

4.2 测试作业实现

接下来测试 StatusWorker 模块。可以直接调用 perform/1 函数进行测试。以下是测试代码:

# phone_app/test/phone_app/conversations/worker/status_worker_test.exs
defmodule PhoneApp.Conversations.Worker.StatusWorkerTest do
  use PhoneApp.DataCase, async: true
  alias PhoneApp.Conversations.Worker.StatusWorker
  alias Test.Factory.SmsMessageFactory

  defp setup_bypass(message, status: status) do
    bypass = Bypass.open()
    Process.put(:twilio_base_url, "http://localhost:#{bypass.port}")

    Bypass.expect_once(
      bypass,
      "GET",
      "/Accounts/account_sid/Messages/#{message.message_sid}.json",
      fn conn ->
        conn
        |> Plug.Conn.put_resp_header("Content-Type", "application/json")
        |> Plug.Conn.resp(200, Jason.encode!(%{status: status}))
      end
    )
  end

  test "a message status is updated" do
    message = SmsMessageFactory.create()
    setup_bypass(message, status: "delivered")

    assert {:ok, job} = StatusWorker.enqueue(message)
    assert {:ok, updated} = StatusWorker.perform(job)

    assert updated.status == "delivered"
    assert Repo.reload(message) == updated
  end

  test "not ready yet, enqueue" do
    message = SmsMessageFactory.create()
    setup_bypass(message, status: "queued")

    assert {:ok, job} = StatusWorker.enqueue(message)
    assert StatusWorker.perform(job) == {:error, "Message not ready"}
    assert Repo.reload(message) == message
  end
end
  • setup_bypass/2 函数:用于设置 Bypass 模拟 HTTP 请求。
  • 测试流程:先使用 StatusWorker.enqueue/1 入队作业,再使用 StatusWorker.perform/1 执行作业,并验证结果。

4.3 测试 Oban 作业流程

graph TD;
    A[引入 Oban 测试模块] --> B[测试作业入队];
    B --> C[测试作业实现];
    C --> D[验证作业结果];

5. 总结

测试对于生产应用至关重要,它能让我们确信应用按预期工作并能正确处理错误。在 Elixir 中,我们可以利用多种工具和方法进行测试:
- ExUnit :Elixir 内置的测试框架,支持模式匹配,能清晰地指出断言不匹配的差异。
- Phoenix 辅助模块 DataCase ConnCase 分别用于测试查询和端点。
- 第三方库 Bypass 用于模拟外部 API 请求, Oban.Testing 用于测试 Oban 作业。

通过本文介绍的各种测试方法,我们可以为应用构建坚实的测试基础,确保应用的稳定性和可靠性。在实际开发中,应尽早开始编写测试,以提高开发效率和代码质量。

5.1 测试工具和场景总结

测试工具/场景 功能描述 示例代码位置
ExUnit Elixir 内置测试框架,支持模式匹配 贯穿各测试模块
Phoenix DataCase 用于测试 Ecto 查询 ContactStoreTest 模块
Phoenix ConnCase 用于测试 Phoenix 端点 MessageControllerTest 模块
Bypass 模拟外部 API 请求 ConversationsTest 模块
Oban.Testing 测试 Oban 作业入队和执行 ConversationsTest StatusWorkerTest 模块

5.2 测试建议

  • 尽早开始测试 :在开发过程中尽早编写测试,能及时发现和修复问题。
  • 全面覆盖 :测试应覆盖各种正常和异常情况,确保应用的健壮性。
  • 使用辅助工具 :合理使用测试工厂、模拟库等辅助工具,简化测试代码。
【RIS 辅助的 THz 混合场波束斜视下的信道估计与定位】在混合场波束斜视效应下,利用太赫兹超大可重构智能表面感知用户信道与位置(Matlab代码实现)内容概要:本文围绕“IS 辅助的 THz 混合场波束斜视下的信道估计与定位”展开,重点研究在太赫兹(THz)通信系统中,由于混合近场与远场共存导致的波束斜视效应下,如何利用超大可重构智能表面(RIS)实现对用户信道状态信息和位置的联合感知与精确估计。文中提出了一种基于RIS调控的信道参数估计算法,通过优化RIS相移矩阵提升信道分辨率,并结合信号到达角(AoA)、到达时间(ToA)等信息实现高精度定位。该方法在Matlab平台上进行了仿真验证,复现了SCI一区论文的核心成果,展示了其在下一代高频通信系统中的应用潜力。; 适合人群:具备通信工程、信号处理或电子信息相关背景,熟悉Matlab仿真,从事太赫兹通信、智能反射面或无线定位方向研究的研究生、科研人员及工程师。; 使用场景及目标:① 理解太赫兹通信中混合场域波束斜视问题的成因与影响;② 掌握基于RIS的信道估计与用户定位联合实现的技术路径;③ 学习并复现高水平SCI论文中的算法设计与仿真方法,支撑学术研究或工程原型开发; 阅读建议:此资源以Matlab代码实现为核心,强调理论与实践结合,建议读者在理解波束成形、信道建模和参数估计算法的基础上,动手运行和调试代码,深入掌握RIS在高频通信感知一体化中的关键技术细节。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值