使用 ExUnit 进行测试时如何伪造 IO 输入?
How do I fake IO input when testing with ExUnit?
我有一个 Elixir 程序,我想测试它通过 IO.gets
多次从用户那里获得输入。我将如何在测试中伪造这个输入?
注意:我想 return 每个 IO.gets
不同的值
首选的方法是将代码分成纯代码(没有副作用)和不纯代码(有 io)。因此,如果您的代码如下所示:
IO.gets
...
...
...
IO.gets
...
...
尝试将 IO.gets 之间的部分提取到可以与 IO.gets
隔离测试的函数中:
def fun_to_test do
input1 = IO.gets
fun1(input1)
input2 = IO.gets
fun2(input2)
end
然后您可以单独测试功能。这并不总是最好的做法,尤其是当不纯的部分深入 if
、case
或 cond
语句时。
另一种方法是将 IO
作为显式依赖项传递:
def fun_to_test(io \ IO) do
io.gets
...
...
...
io.gets
...
...
end
这样您就可以在生产代码中使用它而无需任何修改,但在您的测试中您可以将它传递给一些不同的模块 fun_to_test(FakeIO)
。如果提示不同,您可以在 gets
参数上进行模式匹配。
defmodule FakeIO do
def gets("prompt1"), do: "value1"
def gets("prompt2"), do: "value2"
end
如果它们始终相同,您需要保持 gets
被调用次数的状态:
defmodule FakeIO do
def start_link do
Agent.start_link(fn -> 1 end, name: __MODULE__)
end
def gets(_prompt) do
times_called = Agent.get_and_update(__MODULE__, fn state ->
{state, state + 1}
end)
case times_called do
1 -> "value1"
2 -> "value2"
end
end
end
最后一个实现是一个具有内部状态的完全可用的模拟。在测试中使用它之前,您需要先调用 FakeIO.start_link
。如果这是你需要在很多地方做的事情,你可以考虑一些模拟库,但正如你所看到的——这并不太复杂。为了使 FakeIO
更好,您可以打印提示。我在这里跳过了这个细节。
发现已接受答案中的 FakeIO 解决方案非常有帮助。希望添加另一个清晰的示例,并指出在需要时从 FakeIO 委托给真实的 IO
在这里,我有一个简单的要求,即编写一个执行少量 IO 的应用程序,从 STDIN 读取名称并在 STDOUT 上回复。
示例输出
What is your name? Elixir
Hello, Elixir, nice to meet you!
下面是“应用程序”,一个名为 Ex1
:
的模块
defmodule Ex1 do
def sayHello(io \ IO) do
"What is your name? "
|> input(io)
|> reply
|> output(io)
end
def input(message, io \ IO) do
io.gets(message) |> String.trim
end
def reply(name) do
"Hello, #{name}, nice to meet you!"
end
def output(message, io \ IO) do
io.puts(message)
end
end
及相关测试:
defmodule FakeIO do
defdelegate puts(message), to: IO
def gets("What is your name? "), do: "Elixir "
def gets(value), do: raise ArgumentError, message: "invalid argument #{value}"
end
defmodule Ex1Test do
use ExUnit.Case
import ExUnit.CaptureIO
doctest Ex1
@tag runnable: true
test "input" do
assert Ex1.input("What is your name? ", FakeIO) == "Elixir"
end
@tag runnable: true
test "reply" do
assert Ex1.reply("Elixir") == "Hello, Elixir, nice to meet you!"
end
@tag runnable: true
test "output" do
assert capture_io(fn ->
Ex1.output("Hello, Elixir, nice to meet you!", FakeIO)
end) == "Hello, Elixir, nice to meet you!\n"
end
@tag runnable: true
test "sayHello" do
assert capture_io(fn ->
Ex1.sayHello(FakeIO)
end) == "Hello, Elixir, nice to meet you!\n"
end
end
有趣的部分是 FakeIO
结合参数模式匹配和 defdelegate
委托给真正的 IO.puts
调用。 gets
上有一个“包罗万象”的模式,如果传递给 FakeIO 的不是预期的 gets 参数,则引发 ArgumentError。
defmodule FakeIO do
defdelegate puts(message), to: IO
def gets("What is your name? "), do: "Elixir "
def gets(value), do: raise ArgumentError, message: "invalid argument #{value}"
end
无论如何,希望这能提供一些关于 FakeIO 使用的见解。
我有一个 Elixir 程序,我想测试它通过 IO.gets
多次从用户那里获得输入。我将如何在测试中伪造这个输入?
注意:我想 return 每个 IO.gets
首选的方法是将代码分成纯代码(没有副作用)和不纯代码(有 io)。因此,如果您的代码如下所示:
IO.gets
...
...
...
IO.gets
...
...
尝试将 IO.gets 之间的部分提取到可以与 IO.gets
隔离测试的函数中:
def fun_to_test do
input1 = IO.gets
fun1(input1)
input2 = IO.gets
fun2(input2)
end
然后您可以单独测试功能。这并不总是最好的做法,尤其是当不纯的部分深入 if
、case
或 cond
语句时。
另一种方法是将 IO
作为显式依赖项传递:
def fun_to_test(io \ IO) do
io.gets
...
...
...
io.gets
...
...
end
这样您就可以在生产代码中使用它而无需任何修改,但在您的测试中您可以将它传递给一些不同的模块 fun_to_test(FakeIO)
。如果提示不同,您可以在 gets
参数上进行模式匹配。
defmodule FakeIO do
def gets("prompt1"), do: "value1"
def gets("prompt2"), do: "value2"
end
如果它们始终相同,您需要保持 gets
被调用次数的状态:
defmodule FakeIO do
def start_link do
Agent.start_link(fn -> 1 end, name: __MODULE__)
end
def gets(_prompt) do
times_called = Agent.get_and_update(__MODULE__, fn state ->
{state, state + 1}
end)
case times_called do
1 -> "value1"
2 -> "value2"
end
end
end
最后一个实现是一个具有内部状态的完全可用的模拟。在测试中使用它之前,您需要先调用 FakeIO.start_link
。如果这是你需要在很多地方做的事情,你可以考虑一些模拟库,但正如你所看到的——这并不太复杂。为了使 FakeIO
更好,您可以打印提示。我在这里跳过了这个细节。
发现已接受答案中的 FakeIO 解决方案非常有帮助。希望添加另一个清晰的示例,并指出在需要时从 FakeIO 委托给真实的 IO
在这里,我有一个简单的要求,即编写一个执行少量 IO 的应用程序,从 STDIN 读取名称并在 STDOUT 上回复。
示例输出
What is your name? Elixir
Hello, Elixir, nice to meet you!
下面是“应用程序”,一个名为 Ex1
:
defmodule Ex1 do
def sayHello(io \ IO) do
"What is your name? "
|> input(io)
|> reply
|> output(io)
end
def input(message, io \ IO) do
io.gets(message) |> String.trim
end
def reply(name) do
"Hello, #{name}, nice to meet you!"
end
def output(message, io \ IO) do
io.puts(message)
end
end
及相关测试:
defmodule FakeIO do
defdelegate puts(message), to: IO
def gets("What is your name? "), do: "Elixir "
def gets(value), do: raise ArgumentError, message: "invalid argument #{value}"
end
defmodule Ex1Test do
use ExUnit.Case
import ExUnit.CaptureIO
doctest Ex1
@tag runnable: true
test "input" do
assert Ex1.input("What is your name? ", FakeIO) == "Elixir"
end
@tag runnable: true
test "reply" do
assert Ex1.reply("Elixir") == "Hello, Elixir, nice to meet you!"
end
@tag runnable: true
test "output" do
assert capture_io(fn ->
Ex1.output("Hello, Elixir, nice to meet you!", FakeIO)
end) == "Hello, Elixir, nice to meet you!\n"
end
@tag runnable: true
test "sayHello" do
assert capture_io(fn ->
Ex1.sayHello(FakeIO)
end) == "Hello, Elixir, nice to meet you!\n"
end
end
有趣的部分是 FakeIO
结合参数模式匹配和 defdelegate
委托给真正的 IO.puts
调用。 gets
上有一个“包罗万象”的模式,如果传递给 FakeIO 的不是预期的 gets 参数,则引发 ArgumentError。
defmodule FakeIO do
defdelegate puts(message), to: IO
def gets("What is your name? "), do: "Elixir "
def gets(value), do: raise ArgumentError, message: "invalid argument #{value}"
end
无论如何,希望这能提供一些关于 FakeIO 使用的见解。