问题
I have an Elixir program I'd like to test which gets input from the user via IO.gets
several times. How would I go about faking this input in a test?
Note: I would like to return a different value for each IO.gets
回答1:
The preferred way to do it is to split your code into pure (no side effects) and impure (does the io). So if your code looks like this:
IO.gets
...
...
...
IO.gets
...
...
try to extract the parts between IO.gets into functions that you can test in isolation from IO.gets
:
def fun_to_test do
input1 = IO.gets
fun1(input1)
input2 = IO.gets
fun2(input2)
end
and then you can test the functions in isolation. This isn't always the best thing to do, especially if the impure parts are deep inside if
, case
or cond
statements.
The alternative is to pass the IO
as an explicit dependency:
def fun_to_test(io \\ IO) do
io.gets
...
...
...
io.gets
...
...
end
This way you can use it from you production code without any modification, but in your test you can pass it some different module fun_to_test(FakeIO)
. If the prompts are different you can just pattern match on the gets
argument.
defmodule FakeIO do
def gets("prompt1"), do: "value1"
def gets("prompt2"), do: "value2"
end
If they are always the same you need to keep the state of how many times the gets
was called:
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
This last implementation is a fully working mock with its internal state. You need to call FakeIO.start_link
before using it in the test. If this is what you need to do in many places you may consider some mocking library, but as you can see - this isn't too complicated. To make the FakeIO
even better you can print the prompt. I skipped this detail here.
回答2:
Found the FakeIO solution in the accepted answer very helpful. Wished to add another clear example, and also point out delegation from FakeIO to the real IO when needed
Here, I have a simple requirement to write an App that does a little IO, reading a name from STDIN and replying on STDOUT.
Example Output
What is your name? Elixir
Hello, Elixir, nice to meet you!
Below is the "app", a single module called 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
And associated tests:
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
The interesting part is the usage of FakeIO
in conjunction with parameter pattern matching and defdelegate
to delegate to the real IO.puts
call. There is a "catchall" pattern on gets
to raise an ArgumentError if anything other than the expected gets param is passed to 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
Anyhow, hope this offers some insights in regards to FakeIO usage.
来源:https://stackoverflow.com/questions/37715885/how-do-i-fake-io-input-when-testing-with-exunit