Testing escripts with Sytem.exit()

Hello all.

When writing an Escript that uses System.exit/1 Is there a way to test it?
Or is there a better way to make an Escript return the proper exit code which is testable with ExUnit?

Thanks

You could invoke the script via System.shell/2, which returns a tuple containing the executed command’s output and exit status.

EDIT: Sorry, I misread the question and thought you were looking to test exit status. Better answer coming soon.

Assuming you’re writing this script in a full mix-based environment, I’d probably test this one (or both!) of two ways:

The first would be to make the escript itself a very thin wrapper around the actual functionality it invokes, so that functionality can be tested with more conventional unit tests. You can use Code.compile_file/2 to load and compile your .exs in the test setup, call its main function yourself from inside the test, and unload it after:

defmodule MyScriptTest do
  use ExUnit.Case

  setup do
    Code.compile_file("path/to/escript.exs")
    on_exit(fn ->
      :code.delete(MyEscriptModule)
      :code.purge(MyEscriptModule)
    end)
  end

  test "test the thing" do
    with result <- MyEscriptModule.CLI.main(args) do
      assert something_about(result)
    end
  end
end

The second would be to wrap the System.exit/1 call in its own tiny module:

defmodule MyEscriptModule do
  defmodule Terminator do
    def terminate(status), do: System.exit(status)
  end

  defmodule CLI do
    def main(args) do
      # ... do whatever your script does
      terminate(some_status)
    do

    defp terminate(status) do
      Application.get_env(:my_app, :terminator, Terminator)
      |> Kernel.apply(:terminate, [status])
    end
  end
end

Then in your test, create a mock terminator module you can spy on:

defmodule MyScriptTest do
  use ExUnit.Case

  setup do
    code =
      quote do
        def terminate(status), do: unquote(self()) |> send({:exit_code, status})
      end

    Module.create(TestTerminator, code, file: __ENV__.file)
    Application.put_env(:my_app, :terminator, TestTerminator)

    on_exit(fn ->
      Application.delete_env(:my_app, :terminator)
      :code.delete(TestTerminator)
      :code.purge(TestTerminator)
    end)
  end

  test "test the thing" do
    with result <- MyEscriptModule.CLI.main(args) do
      assert_received({:exit_code, expected_exit_status})
    end
  end
end

There’s probably a much simpler way to structure this, especially the TestTerminator, but I don’t know how you’d get the test process’ PID into the mock without constructing it that way. Someone else might have a tighter example.

1 Like