我正在编写一个exs
文件的Elixir脚本(不使用mix
)。该脚本包含一个模块以及外部作用域中的一个函数调用,该函数调用开始接受stdin
的输入并将其发送到模块函数。
我还有另一个文件,其中包含我的所有单元测试。但是,我遇到两个问题:
stin
等待输入时,直到我按Ctrl + D(输入结束),ExUnit测试才会完成。我希望在模块中的各个功能上运行测试,而无需运行实际的应用程序。stdout
上的输出与stdin
上的各种输入。可以用ExUnit完成吗?答案 0 :(得分:1)
当程序等待输入时,ExUnit测试不会 完成,直到我按Ctrl + D(输入结束)。我想要跑步 在我的模块中测试单个函数而不运行 实际的应用程序。
思考mocks。
该脚本包含一个模块以及外部作用域中的一个函数 开始接受来自stdin的输入并将其发送到模块 功能。
我认为这不是测试的良好结构。相反,您应该安排这样的事情:
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
换句话说:
通常,您要测试函数的返回值,例如上面的start()
。但是,根据您的情况,您还需要测试other_func()
发送到stdout的输出。 ExUnit具有以下功能:capture_io。
这是我第一次尝试mox。要使用mox
模拟功能,您的模块需要实现behaviour
。行为仅说明模块必须定义的功能。这是一个行为定义,用于指定我要模拟的功能:
foo / lib / my_io.ex:
defmodule Foo.MyIO do
@callback start() :: String.t()
end
String.t()
是字符串的类型说明,而::
右边的术语是函数的返回值,因此start()
不使用args并返回字符串
然后,您指定模块实现该行为:
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()
函数的模拟或仿真,该函数从stdin读取。模拟功能通过返回随机字符串来模拟从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
模块相比,这有何优势。
答案 1 :(得分:1)
据我所知,您必须将代码转换为.ex
文件。这是因为当您需要.exs
文件以对其进行测试时:
$ elixir -r my.exs my_tests.exs
elixir必须执行.exs
文件中的代码-否则,您在该文件中定义的模块将不存在。猜猜执行文件中的代码会发生什么?您在文件的顶层具有以下条件:
My.read_input()
然后read_input()
函数调用IO.gets/1
,该函数将向stdout发送提示,并等待用户输入。当您告诉elixir执行代码时,它就会执行该操作。如果不需要该文件,则在测试文件中,对该模块中所有功能的引用将导致:
(CompileError)my_tests.exs:11:模块My未加载且无法 被发现
答案 2 :(得分:0)
好的,您的要求是:
.exs
文件启动程序。您需要在不运行脚本的情况下针对您的模块运行测试-因为您的脚本暂停了,要求用户从stdin中输入内容。
奖金:而且,您想使用mox
模块进行测试。
我们在这里:
my.exs:
My.go()
my.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
下一步:
.zip
文件移动到脚本所在的目录并解压缩。导航至lib
目录下的mox-master
目录,并将mox.ex
复制到脚本所在的目录。
导航到lib/mox
目录,并将server.ex
复制到脚本所在的目录。
编译mox.ex
,server.ex
和my.ex
:$ elixirc mox.ex server.ex my.ex
运行脚本:
$ elixir my.exs
测试my.ex
:
$ elixir my_tests.ex
您可以按照我的其他答案中的说明对一系列不同的输入进行测试。