我正在编写一个模块来查询在线天气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
Run Code Online (Sandbox Code Playgroud)
在我的测试中,我决定使用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
Run Code Online (Sandbox Code Playgroud)
我决定GenServer使用特定名称注册,原因如下:
某人不太可能需要多个实例
我可以在我的Weather模块中定义一个公共API,它抽象出底层的存在GenServer.用户不必向weather_in函数提供PID /名称以与底层通信GenServer
我可以将我GenServer置于监督树下
当我运行测试时,当它们同时运行时,setup每次测试都会执行一次回调.因此,有并发尝试启动我的服务器,它失败了{:error, {:already_started, #PID<0.133.0>}}.
我问Slack是否有任何我可以做的事情.也许有一个我不知道的惯用解决方案......
总结所讨论的解决方案,在实现和测试时GenServer,我有以下选择:
不使用特定名称注册服务器以使每个测试启动其自己的GenServer实例.服务器的用户可以手动启动它,但是他们必须将它提供给模块的公共API.服务器也可以放在监督树中,即使有名称,但模块的公共API仍然需要知道要与哪个PID通信.给定一个名称作为参数传递,我猜他们可以找到相关的PID(我想OTP可以做到这一点.)
使用特定名称注册服务器(就像我在我的示例中所做的那样).现在只能有一个GenServer实例,测试必须按顺序运行(async: false),每个测试必须启动和终止服务器.
使用特定名称注册服务器.如果测试都针对相同的唯一服务器实例运行,则测试可以并发运行(使用setup_all,对于整个测试用例,实例只能启动一次).然而,imho这是一种错误的测试方法,因为所有测试都将针对同一台服务器运行,改变其状态,从而相互搞乱.
考虑到用户可能不需要创建这个GenServer的多个实例,我很想简单地交换测试并发性并使用解决方案2.
[编辑]尝试解决方案2,但由于同样的原因它仍然失败:already_started.我再次阅读文档async: false,发现它可以防止测试用例与其他测试用例并行运行.它没有像我想象的那样按顺序运行我的测试用例的测试.救命!
Chr*_*yer 25
我注意到的一个关键问题是你有错误的签名handle_call,应该是handle_call(args, from, state)(你现在只有handle_call(args).
我从来没有使用它,但是我发誓,QuickCheck是真正测试GenServers的黄金标准.
在单元测试级别,由于GenServer的功能架构,存在另一种选择:
如果handle_[call|cast|info]使用期望的参数和状态组合测试方法,则不必启动GenServer:使用测试库替换OTP,并调用模块代码,就像它是一个平面库一样.这不会测试你的api函数调用,但是如果你将它们保持为瘦的pass-thru方法,你可以将风险降到最低.
*如果您使用延迟回复,这种方法会遇到一些问题,但您可以用足够的工作对它们进行排序.
我对GenServer进行了一些更改:
新模块:
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
Run Code Online (Sandbox Code Playgroud)
和测试代码:
# 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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
6693 次 |
| 最近记录: |