0
votes

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:

  1. 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 individual functions inside my module without running the actual app.
  2. I would also like to write tests for the CLI interface, checking it's output on stdout vs various inputs on stdin. Can this be done with ExUnit?
3
Possible related question: stackoverflow.com/questions/37715885/…zwippie
The script contains a module, as well one function in the outer scope -- The function is outside the module? That's not possible. Your file cannot be an Elixir script.7stud
Sorry for the confusion, it's a function call, not a function definitionJeremiah Rose
Okay, so it's an .exs file. Are you calling a function inside the module?7stud
Or, is it an anonymous function that calls itself?7stud

3 Answers

2
votes

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:

  1. You should define a function that gets the input--and that's it.
  2. 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.

1
votes

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

0
votes

Okay, your requirements are:

  1. You don't want to use mix.
  2. You want to start your program with an .exs file.
  3. You need to run tests against your module without running your script--because your script halts to ask the user for input from stdin.

  4. 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:

  1. Go to github and click the Clone or Download button for mox.
  2. Move the mox .zip file to the same directory as your script and unzip it.
  3. Navigate to the lib directory under the mox-master directory and copy mox.ex into the same directory as your script.

  4. Navigate to the lib/mox directory and copy server.ex into the same directory as your script.

  5. 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.