【问题标题】:Use ExUnit to test CLI Elixir script使用 ExUnit 测试 CLI Elixir 脚本
【发布时间】:2019-11-20 08:02:13
【问题描述】:

我正在编写一个 exs 文件 Elixir 脚本(不使用 mix)。该脚本包含一个模块,以及一个在外部范围内开始接受来自stdin 的输入并将其发送到模块函数的函数调用。

我还有一个包含所有单元测试的文件。但是,我有两个问题:

  1. 当程序在stin 处等待输入时,ExUnit 测试在我按下 Ctrl+D(输入结束)之前不会完成。我希望在不运行实际应用的情况下对模块内的各个函数运行测试。
  2. 我还想为 CLI 接口编写测试,检查 stdout 上的输出与 stdin 上的各种输入。这可以用 ExUnit 完成吗?

【问题讨论】:

  • 脚本包含一个模块,以及一个在外部范围内的函数 -- 函数在模块之外?那是不可能的。您的文件不能是 Elixir 脚本。
  • 对不起,这是一个函数调用,而不是函数定义
  • 好的,这是一个 .exs 文件。你是在模块内部调用函数吗?
  • 或者,它是一个匿名函数调用自己?

标签: elixir ex-unit


【解决方案1】:

好的,你的要求是:

  1. 您不想使用 mix。
  2. 您想使用.exs 文件启动您的程序。
  3. 您需要在不运行脚本的情况下针对您的模块运行测试——因为您的脚本会暂停以向用户询问来自标准输入的输入。

  4. 奖励:而且,您想使用mox 模块进行测试。

我们开始吧:

我的.exs:

My.go()

我的.ex:

#Define a behavior for mox testing:

defmodule MyIO do
  @callback read_input() :: String.t()
end

# Adopt the behaviour in your module:

defmodule My do
  @behaviour MyIO

  def go do
    read_input()
    |> other_func()
  end

  def read_input do
    IO.gets("enter: ")
  end

  def other_func(str) do
    IO.puts("You entered: #{str}")
  end

end

my_tests.exs:

ExUnit.start()
Mox.Server.start_link([])

defmodule MyTests do
  use ExUnit.Case, async: true
  import ExUnit.CaptureIO

  import Mox
  defmock(MyIOMock, for: MyIO)
  setup :verify_on_exit!

  test "stdin/stdout is correct" do

    MyIOMock
    |> expect(:read_input, fn -> "hello" end)

    assert MyIOMock.read_input() == "hello"

    #Doesn't use mox:
    assert capture_io(fn -> My.other_func("hello") end) 
           == "You entered: hello\n"
  end

end

下一步:

  1. 转到 github 并单击 Clone or Download button 获取 mox。
  2. 将 mox .zip 文件移动到与您的脚本相同的目录并解压缩。
  3. 导航到mox-master 目录下的lib 目录,并将mox.ex 复制到与您的脚本相同的目录中。

  4. 导航到lib/mox 目录并将server.ex 复制到与您的脚本相同的目录中。

  5. 编译mox.exserver.exmy.ex$ elixirc mox.ex server.ex my.ex

运行你的脚本:

$ elixir my.exs

测试my.ex:

$ elixir my_tests.ex

您可以对我的其他答案中演示的不同输入列表进行测试。

【讨论】:

  • 感谢您的帮助 7stud。仅仅为了在一个小脚本上运行简单的测试就需要所有这些工作,这似乎很疯狂——我猜 Elixir 确实是为大型可扩展企业应用程序设计的,我应该停止试图假装它是 Python,因为它不是!
  • @JeremiahRose,我猜 Elixir 确实是为大型可扩展企业应用程序设计的 -- 嗯,erlang 是为运行大型电话系统而创建的,而 erlang 的优势在于并发性,并且据说 elixir 的最佳点是可以扩展的 Web 应用程序,但这并不意味着您不能将 elixir 用于较小的项目......
  • ...而且,mix 非常好用,我不知道你为什么不使用 mix。 mix 根据社区准则构建您的代码,它使编译程序变得容易,并且 mix 处理允许您访问任何文件中的模块的所有路径操作,您可以在 iex 中使用您的应用程序并运行测试.
【解决方案2】:

据我所知,您必须将代码转换为.ex 文件。那是因为当您需要 .exs 文件才能对其进行测试时:

$ elixir -r my.exs my_tests.exs 

elixir 必须执行.exs 文件中的代码——否则您在该文件中定义的模块将不存在。猜猜当你执行文件中的代码时会发生什么?您在文件的顶层有以下内容:

My.read_input()

read_input() 函数调用IO.gets/1 向标准输出发送提示并等待用户输入。当您告诉 elixir 执行代码时,它会执行此操作。如果您不需要该文件,那么在您的测试文件中,您对模块中函数的所有引用都将导致:

(CompileError) my_tests.exs:11: 模块 My 未加载且无法加载 被发现

【讨论】:

    【解决方案3】:

    当程序在 stin 等待输入时,ExUnit 测试不会 完成,直到我按 Ctrl+D(输入结束)。我想运行 在不运行 实际应用。

    想想mocks

    脚本包含一个模块,以及外部范围内的一个函数 开始接受来自标准输入的输入并将其发送到模块 功能。

    我认为这不是一个好的测试结构。相反,您应该这样安排:

    foo/lib/a.x:

    defmodule Foo.A do
    
      def go do
        start()
        |> other_func()
      end
    
      def start do
        IO.gets("enter: ")
      end
    
      def other_func(str) do
        IO.puts("You entered: #{str}")
      end
    
    end
    

    换句话说:

    1. 您应该定义一个获取输入的函数——就是这样。
    2. 您应该定义另一个函数来接受某些输入并执行某些操作。

    通常,您测试函数的返回值,例如上面的start()。但是,在您的情况下,您还需要测试other_func() 发送到标准输出的输出。 ExUnit 有一个功能:capture_io

    这是我第一次尝试mox。要使用mox 模拟函数,您的模块需要实现behaviour。行为只是说明模块必须定义的功能。这是一个行为定义,它指定了我要模拟的函数:

    foo/lib/my_io.ex:

    defmodule Foo.MyIO do
      @callback start() :: String.t()
    end
    

    String.t() 是字符串的类型说明,:: 右边的项是函数的返回值,所以start() 不接受任何参数并返回一个字符串。

    然后您指定您的模块实现该行为:

    defmodule Foo.A do
      @behaviour Foo.MyIO
    
      ...
      ...
    end
    

    通过该设置,您现在可以模拟或模拟行为中指定的任何函数。

    你说你没有使用混合项目,但我是。对不起。

    test/test_helpers.exs:

    ExUnit.start()
    Ecto.Adapters.SQL.Sandbox.mode(Foo.Repo, :manual)
    
    Mox.defmock(Foo.MyIOMock, for: Foo.MyIO)  #(random name, behaviour_definition_module)
    

    test/my_test.exs:

    defmodule MyTest do
      use ExUnit.Case, async: true
    
      import Mox
      import ExUnit.CaptureIO
    
      setup :verify_on_exit!  # For Mox.
    
      test "stdin stdout io" do
    
        Foo.MyIOMock
        |> expect(:start, fn -> "hello" end)
    
        assert Foo.MyIOMock.start() == "hello"
    
        #Doesn't use mox:
        assert capture_io(fn -> Foo.A.other_func("hello") end) 
               == "You entered: hello\n"
    
      end
    
    end
    

    这部分:

      Foo.MyIOMock
      |> expect(:start, fn -> "hello" end)
    

    为从标准输入读取的start() 函数指定模拟或模拟。 mock 函数通过返回一个随机字符串来模拟从 stdin 读取。对于如此简单的事情,这似乎需要做很多工作,但那是测试!如果这太令人眼花缭乱,那么您可以创建自己的模块:

    defmodule MyMocker do
      def start() do
        "hello"
      end    
    end
    

    然后在你的测试中:

     test "stdin stdout io" do
        assert Foo.MyMocker.start() == "hello"
        assert capture_io(fn -> Foo.A.other_func("hello") end) 
               == "You entered: hello\n"
     end
    

    我还想为 CLI 界面编写测试,检查一下 标准输出上的输出与标准输入上的各种输入

    因为匿名函数(fn args -> ... end)是闭包,它们可以看到周围代码中的变量,所以你可以这样做:

        input = "goodbye"
    
        Foo.MyIOMock
        |> expect(:start, fn -> input end)
    
        assert Foo.MyIOMock.start() == input
    
        assert capture_io(fn -> Foo.A.other_func(input) end) 
               == "You entered: #{input}\n"
    

    您也可以这样做:

    inputs = ["hello", "goodbye"]
    
    Enum.each(inputs, fn input ->
    
      Foo.MyIOMock
      |> expect(:start, fn -> input end)
    
      assert Foo.MyIOMock.start() == input
    
      assert capture_io(fn -> Foo.A.other_func(input) end) 
             == "You entered: #{input}\n"
    end)
    

    请注意,这比创建自己的 MyMocker 模块更有优势。

    【讨论】:

    • 不幸的是,这并没有回答我的问题,因为我没有使用mix
    猜你喜欢
    • 1970-01-01
    • 2016-06-28
    • 1970-01-01
    • 2020-03-20
    • 1970-01-01
    • 2013-11-04
    • 1970-01-01
    • 2018-09-20
    • 1970-01-01
    相关资源
    最近更新 更多