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

如何为Elixir GenServer Worker编写地道的集成测试?

为Elixir GenServer Worker编写集成测试

针对你基于GenServer实现的CotacoesFetcher和CotacaoDownloader两个Worker,结合已Mock的外部依赖,以下是编写集成测试的具体方案:

核心思路

集成测试的重点是验证Worker之间的协作逻辑、消息传递流程以及生命周期行为,而非孤立测试单个函数。测试中会启动真实的Worker进程,用Mock替代API、数据库等外部依赖,同时验证Worker间的交互是否符合预期。


一、CotacoesFetcher集成测试

测试覆盖以下场景:触发抓取流程后,是否正确调用依赖、插入数据并通知CotacaoDownloader;无新数据时的日志输出;以及调度下一次执行的逻辑。

测试代码示例

defmodule CotacoesETL.Workers.Pesagro.CotacoesFetcherIntegrationTest do
  use ExUnit.Case, async: false

  alias CotacoesETL.Workers.Pesagro.{CotacoesFetcher, CotacaoDownloader}
  alias Cotacoes.Handlers.CotacaoHandler
  alias CotacoesETL.Integrations.PesagroAPI

  setup do
    # 启动协作的两个Worker进程
    {:ok, fetcher_pid} = CotacoesFetcher.start_link(nil)
    {:ok, downloader_pid} = CotacaoDownloader.start_link(nil)

    # 绑定Mock实现
    Mox.stub_with(PesagroAPIMock, PesagroAPI)
    Mox.stub_with(CotacaoHandlerMock, CotacaoHandler)

    # 测试结束后清理进程
    on_exit(fn ->
      GenServer.stop(fetcher_pid)
      GenServer.stop(downloader_pid)
    end)

    {:ok, fetcher_pid: fetcher_pid, downloader_pid: downloader_pid}
  end

  test "触发抓取后完成全流程并通知Downloader", %{downloader_pid: downloader_pid} do
    # 预设Mock返回值
    test_doc = "<html><a href='test-boletin'>Boletim Teste</a></html>"
    test_link = "test-boletin"
    test_cotacao = %{id: "cot-test-01", link: test_link, baixada?: false}

    # 验证Mock调用
    Mox.expect(PesagroAPIMock, :fetch_document!, fn -> test_doc end)
    Mox.expect(PesagroAPIMock, :fetch_all_links, fn ^test_doc -> [test_link] end)
    Mox.expect(CotacaoHandlerMock, :insert_cotacao_pesagro, fn ^test_link, _today -> {:ok, test_cotacao} end)
    Mox.expect(CotacaoHandlerMock, :get_cotacao_file_base_name, fn ^test_cotacao -> "boletim-teste" end)

    # 触发抓取并监听Downloader消息
    CotacoesFetcher.trigger_fetching()
    assert_receive {:cast, CotacaoDownloader, {:download, ^test_cotacao}}, 1000
  end

  test "无新报价时输出正确日志" do
    # 预设无链接返回
    Mox.expect(PesagroAPIMock, :fetch_document!, fn -> "<html></html>" end)
    Mox.expect(PesagroAPIMock, :fetch_all_links, fn _ -> [] end)

    # 捕获并验证日志
    assert capture_log(fn ->
      CotacoesFetcher.trigger_fetching()
      Process.sleep(500)
    end) =~ "Nenhuma nova cotação encontrada em Pesagro"
  end
end

二、CotacaoDownloader集成测试

测试覆盖以下场景:处理下载消息时根据文件类型通知对应Worker;收到PDF转换完成消息后通知Ingester;处理ZIP解压完成后的批量转换请求。

测试代码示例

defmodule CotacoesETL.Workers.Pesagro.CotacaoDownloaderIntegrationTest do
  use ExUnit.Case, async: false

  alias CotacoesETL.Workers.Pesagro.{CotacaoDownloader, CotacaoIngester}
  alias CotacoesETL.Workers.{PDFConverter, ZIPExtractor}
  alias Cotacoes.Models.Cotacao
  alias CotacoesETL.Handlers.PesagroHandler
  alias CotacoesETL.Logic.PesagroLogic
  alias CotacoesETL.Events.{PDFConvertedEvent, IngestCotacaoEvent}

  setup do
    {:ok, downloader_pid} = CotacaoDownloader.start_link(nil)

    # 绑定Mock实现
    Mox.stub_with(PesagroHandlerMock, PesagroHandler)
    Mox.stub_with(PesagroLogicMock, PesagroLogic)

    on_exit(fn -> GenServer.stop(downloader_pid) end)
    {:ok, downloader_pid: downloader_pid}
  end

  test "处理PDF类型报价并通知PDFConverter" do
    test_cotacao = %Cotacao{id: "cot-pdf-01", link: "pdf-link", baixada?: false, tipo: :pdf}
    test_file = "/tmp/test.pdf"

    Mox.expect(PesagroHandlerMock, :download_cotacao_from_pesagro!, fn _path, ^test_cotacao ->
      {test_cotacao, test_file}
    end)

    GenServer.cast(CotacaoDownloader, {:download, test_cotacao})
    assert_receive {:cast, PDFConverter, {:convert, event}}, 1000
    assert event.cotacao.id == test_cotacao.id
    assert event.pdf_path == test_file
  end

  test "处理ZIP类型报价并通知ZIPExtractor" do
    test_cotacao = %Cotacao{id: "cot-zip-01", link: "zip-link", baixada?: false, tipo: :zip}
    test_file = "/tmp/test.zip"

    Mox.expect(PesagroHandlerMock, :download_cotacao_from_pesagro!, fn _path, ^test_cotacao ->
      {test_cotacao, test_file}
    end)

    GenServer.cast(CotacaoDownloader, {:download, test_cotacao})
    assert_receive {:cast, ZIPExtractor, {:extract, event}}, 1000
    assert event.zip_path == test_file
    assert event.destination_path == Path.join("/tmp/peapescarte/cotacoes/pesagro/", test_cotacao.id)
  end

  test "收到PDF转换完成消息后通知Ingester" do
    test_cotacao = %Cotacao{id: "cot-test-01"}
    converted_file = "/tmp/converted.txt"
    pdf_event = %PDFConvertedEvent{cotacao: test_cotacao, file_path: converted_file}

    Process.send(CotacaoDownloader, {:pdf_converted, pdf_event}, [])
    assert_receive {:send, CotacaoIngester, {:ingest, %IngestCotacaoEvent{} = ingest_event}}, 1000
    assert ingest_event.cotacao.id == test_cotacao.id
    assert ingest_event.file_path == converted_file
  end
end

关键注意事项

  1. 进程管理:测试中启动真实Worker进程,测试结束后务必停止进程避免资源泄漏。
  2. Mock验证:使用Mox等工具预设Mock返回值,并验证依赖的调用参数、次数是否符合预期。
  3. 消息监听:通过assert_receive验证Worker之间的消息传递是否正确。
  4. 日志验证:用capture_log捕获日志,验证异常或特殊场景下的输出。
  5. 异步控制:必要时用Process.sleep等待异步流程完成,避免测试超时或断言失败。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 20:16:58