gpt4 book ai didi

elixir - Elixir 中 GenServer 的惯用测试策略是什么?

转载 作者:行者123 更新时间:2023-12-03 11:26:36 25 4
gpt4 key购买 nike

我正在编写一个模块来查询在线天气 API。我决定将它作为一个应用程序来实现,并带有一个受监督的 GenServer .

这是代码:

defmodule Weather do
use GenServer

def start_link() do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

def weather_in(city, country) do
GenServer.call(__MODULE__, {:weather_in, city, country_code})
end

def handle_call({:weather_in, city, country}) do
# response = call remote api
{:reply, response, nil}
end
end

在我的测试中,我决定使用 setup回调以启动服务器:
defmodule WeatherTest do
use ExUnit.Case

setup do
{:ok, genserver_pid} = Weather.start_link
{:ok, process: genserver_pid}
end

test "something" do
# assert something using Weather.weather_in
end

test "something else" do
# assert something else using Weather.weather_in
end
end

我决定注册 GenServer使用特定名称有几个原因:
  • 不太可能有人需要多个实例
  • 我可以在我的 Weather 中定义一个公共(public) API抽象底层 GenServer 存在的模块.用户不必为 weather_in 提供 PID/名称。与底层通信的函数GenServer
  • 我可以把我的GenServer在监督树下

  • 当我运行测试时,因为它们同时运行, setup每个测试执行一次回调。因此,有并发尝试启动我的服务器,它失败了 {:error, {:already_started, #PID<0.133.0>}} .

    我在 Slack 上询问是否有什么我可以做的。也许有一个我不知道的惯用解决方案......

    总结讨论的解决方案,在实现和测试 GenServer 时,我有以下选择:
  • 不使用特定名称注册服务器以让每个测试启动它自己的 GenServer 实例。
    服务器的用户可以手动启动它,但他们必须将它提供给模块的公共(public) API。服务器也可以放置在监督树中,即使有名称,但模块的公共(public) API 仍然需要知道要与哪个 PID 通信。给定一个作为参数传递的名称,我猜他们可以找到相关的 PID(我想 OTP 可以做到这一点。)
  • 使用特定名称注册服务器(就像我在示例中所做的那样)。现在只能有一个 GenServer 实例,测试必须按顺序运行( async: false )并且每个测试必须开始 终止服务器。
  • 使用特定名称注册服务器。如果测试都针对同一个唯一的服务器实例运行,则它们可以同时运行(使用 setup_all,对于整个测试用例,一个实例只能启动一次)。然而,恕我直言,这是一种错误的测试方法,因为所有测试都将针对同一台服务器运行,改变其状态并因此相互混淆。

  • 考虑到用户可能不需要创建这个 GenServer 的多个实例,我很想为了简单而交换测试并发并使用解决方案 2。

    [编辑]
    尝试解决方案 2,但由于同样的原因仍然失败 :already_started .我再次阅读了有关 async: false 的文档并发现它阻止了 测试用例 与其他测试用例并行运行。它没有像我想的那样按顺序运行我的测试用例的测试。
    帮助!

    最佳答案

    我注意到的一个关键问题是您的 handle_call 签名错误。 ,应该是 handle_call(args, from, state) (您目前只有 handle_call(args)

    我从未使用过它,但我仰慕的人发誓 QuickCheck 是真正测试 GenServer 的黄金标准。

    在单元测试级别,由于 GenServer 的功能架构,存在另一个选项:

    如果您测试 handle_[call|cast|info]具有预期参数和状态组合的方法,您不必*启动 GenServer:使用您的测试库来替换 OTP,并像调用平面库一样调用您的模块代码。这不会测试您的 api 函数调用,但如果您将它们保留为精简的传递方法,则可以将风险降至最低。

    *如果你使用延迟回复,你会遇到一些问题,但你可以通过足够的工作来解决它们。

    我对您的 GenServer 进行了一些更改:

  • 你的模块不使用它的状态,所以我通过添加一个替代的高级 web 服务,从测试的角度使它变得更有趣。
  • 我更正了句柄调用签名
  • 我添加了一个内部状态模块来跟踪状态。即使在没有状态的 GenServers 上,我总是创建这个模块以备后用,当我不可避免地添加状态时。

  • 新模块:
    defmodule Weather do
    use GenServer

    def start_link() do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end

    def weather_in(city, country) do
    GenServer.call(__MODULE__, {:weather_in, city, country_code})
    end

    def upgrade, do: GenServer.cast(__MODULE__, :upgrade)

    def downgrade, do: GenServer.cast(__MODULE__, :downgrade)

    defmodule State do
    defstruct url: :regular
    end

    def init([]), do: {:ok, %State{}}

    def handle_cast(:upgrade, state) do
    {:noreply, %{state|url: :premium}}
    end
    def handle_cast(:downgrade, state) do
    {:noreply, %{state|url: :regular}}
    end

    # Note the proper signature for handle call:
    def handle_call({:weather_in, city, country}, _from, state) do
    response = case state.url do
    :regular ->
    #call remote api
    :premium ->
    #call premium api
    {:reply, response, state}
    end
    end

    和测试代码:
    # assumes you can mock away your actual remote api calls
    defmodule WeatherStaticTest do
    use ExUnit.Case, async: true

    #these tests can run simultaneously
    test "upgrade changes state to premium" do
    {:noreply, new_state} = Weather.handle_cast(:upgrade, %Weather.State{url: :regular})
    assert new_state.url == :premium
    end
    test "upgrade works even when we are already premium" do
    {:noreply, new_state} = Weather.handle_cast(:upgrade, %Weather.State{url: :premium})
    assert new_state.url == :premium
    end
    # etc, etc, etc...
    # Probably something similar here for downgrade

    test "weather_in using regular" do
    state = %Weather.State{url: :regular}
    {:reply, response, newstate} = Weather.handle_call({:weather_in, "dallas", "US"}, nil, state)
    assert newstate == state # we aren't expecting changes
    assert response == "sunny and hot"
    end
    test "weather_in using premium" do
    state = %Weather.State{url: :premium}
    {:reply, response, newstate} = Weather.handle_call({:weather_in, "dallas", "US"}, nil, state)
    assert newstate == state # we aren't expecting changes
    assert response == "95F, 30% humidity, sunny and hot"
    end
    # etc, etc, etc...
    end

    关于elixir - Elixir 中 GenServer 的惯用测试策略是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33018952/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com