使用 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

然后您可以单独测试功能。这并不总是最好的做法,尤其是当不纯的部分深入 ifcasecond 语句时。

另一种方法是将 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 使用的见解。