Reputation: 4132
I'm writing a single exs
file Elixir script (not using mix
). The script contains a module, as well one function call in the outer scope that starts accepting input from stdin
and sends it to the module functions.
I also have a second file that contains all my unit tests. However, I am having two problems:
stin
the ExUnit tests do not complete until I press Ctrl+D (end of input). I would like the run the tests on the individual functions inside my module without running the actual app.stdout
vs various inputs on stdin
. Can this be done with ExUnit?Upvotes: 0
Views: 856
Reputation: 48649
Okay, your requirements are:
.exs
file.You need to run tests against your module without running your script--because your script halts to ask the user for input from stdin.
Bonus: And, you want to use the mox
module for testing.
Here we go:
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
Next:
.zip
file to the same directory as your script and unzip it.Navigate to the lib
directory under the mox-master
directory and copy mox.ex
into the same directory as your script.
Navigate to the lib/mox
directory and copy server.ex
into the same directory as your script.
Compile mox.ex
, server.ex
, and my.ex
: $ elixirc mox.ex server.ex my.ex
To run your script:
$ elixir my.exs
to test my.ex
:
$ elixir my_tests.ex
You can do the testing for a list of different inputs as demonstrated in my other answer.
Upvotes: 0
Reputation: 48649
As far as I can tell, you have to convert your code to an .ex
file. That's because when you require your .exs
file in order to run tests against it:
$ elixir -r my.exs my_tests.exs
elixir has to execute the code in the .exs
file--otherwise the module you define in that file won't exist. Guess what happens when you execute the code in your file? You have the following at the top level of your file:
My.read_input()
And the read_input()
function calls IO.gets/1
which sends a prompt to stdout and waits for user input. When you tell elixir to execute code, it does that. If you don't require the file, then in your test file all your references to the functions in the module will result in:
(CompileError) my_tests.exs:11: module My is not loaded and could not be found
Upvotes: 1
Reputation: 48649
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:
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.
Upvotes: 2