【问题标题】:How do I fake IO input when testing with ExUnit?使用 ExUnit 进行测试时如何伪造 IO 输入?
【发布时间】:2016-10-09 12:17:01
【问题描述】:

我有一个 Elixir 程序,我想测试它多次通过IO.gets 从用户那里获得输入。我将如何在测试中伪造这个输入?

注意:我想为每个 IO.gets 返回一个不同的值

【问题讨论】:

    标签: testing functional-programming elixir ex-unit


    【解决方案1】:

    在接受的答案中发现 FakeIO 解决方案非常有帮助。希望再添加一个清晰的示例,并在需要时指出从 FakeIO 到真实 IO 的委托

    在这里,我有一个简单的需求,即编写一个执行少量 IO、从 STDIN 读取名称并在 STDOUT 上回复的 App。

    示例输出

    你叫什么名字?灵药

    你好,Elixir,很高兴认识你!

    下面是“app”,一个叫做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 上有一个“catchall”模式来引发 ArgumentError,如果将预期的 get 参数以外的任何内容传递给 FakeIO。

     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 使用的见解。

    【讨论】:

      【解决方案2】:

      这样做的首选方法是将代码拆分为纯代码(无副作用)和不纯代码(执行 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 更好,您可以打印提示。我在这里跳过了这个细节。

      【讨论】:

      • 很好的答案!谢谢
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-28
      • 2014-01-01
      • 2021-09-20
      • 1970-01-01
      • 2017-04-09
      • 1970-01-01
      相关资源
      最近更新 更多