When the program waits for input at stin the ExUnit tests do not
complete until I press Ctrl+D (end of input). I would like the run the
tests on the inividual functions inside my module without running the
actual app.
Think mocks.
The script contains a module, as well one function in the outer scope
that starts accepting input from stdin and sends it to the module
functions.
I don't think that is a good structure for testing. Instead, you should arrange things like this:
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
In other words:
- You should define a function that gets the input--and that's it.
- You should define another function that accepts some input and does something.
Typically, you test the return value of a function, like start()
above. But, in your case you also need to test the output that other_func()
sends to stdout. ExUnit has a function for that: capture_io.
This is my first attempt with mox. To mock a function with mox
, your module needs to implement a behaviour
. A behaviour just states the functions that a module must define. Here's a behaviour definition that specifies the function that I want to mock:
foo/lib/my_io.ex:
defmodule Foo.MyIO do
@callback start() :: String.t()
end
String.t()
is the type specification for a string, and the term to the right of the ::
is the return value of the function, so start()
takes no args and returns a string.
Then you specify that your module implements that behaviour:
defmodule Foo.A do
@behaviour Foo.MyIO
...
...
end
With that setup, you can now mock, or simulate, any of the functions specified in the behavior.
You said you aren't using a mix project, but I am. Sorry.
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
This part:
Foo.MyIOMock
|> expect(:start, fn -> "hello" end)
specifies the mock, or simulation, for the start()
function, which reads from stdin. The mock function simulates reading from stdin by just returning a random string. That may seem like a lot of work for something so simplistic, but that's testing! If that's too bewildering, then you can just create your own module:
defmodule MyMocker do
def start() do
"hello"
end
end
Then in your tests:
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
I would also like to write tests for the CLI interface, checking it's
output on stdout vs various inputs on stdin
Because anonymous functions (fn args -> ... end
) are closures, they can see the variables in the surrounding code, so you can do this:
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"
You can also do this:
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)
Note how that's an advantage over creating your own MyMocker
module.