DEV Community

Marcio Lopes de Faria
Marcio Lopes de Faria

Posted on • Edited on

13 1

Execute and Watch Elixir tests from iex.

EDIT: I came up with a even better way to debug Elixir code than using iex, check it here.

Interactive Driven Development, or REPL Driven Development is a really good way to approach programming, specially for developers that get distracted easily with time being expend out the main development loop or flow.

A way to do it in Elixir is just to use the fswatch as presented in Elixir Mix Test documentation, or using the excellent mix.test-watch library, but them don't work well with iex, the Elixir REPL, so why not do something else.

So I created some modules that can be imported on your iex through the ~/.iex.exs file and get the same benefits.

Just insert it on your ~/.iex.exs file:

Code.compiler_options(ignore_module_conflict: true)
Code.compile_file("~/.iex/iex_watch_tests.exs", File.cwd!())
unless GenServer.whereis(IExWatchTests) do
{:ok, pid} = IExWatchTests.start_link()
# Process will not exit when the iex goes out
Process.unlink(pid)
end
import IExWatchTests.Helpers
view raw .iex.exs hosted with ❤ by GitHub

And create a ~/.iex directory, placing this file on it:

defmodule IExWatchTests do
use GenServer
@moduledoc """
This utility allows to get the same effect of using the
`fcwatch | mix test --stale --listen-on-stdin`, but with
more controll and without the starting time penalty.
The best way to use it is to place it on a directory under
the `~/.iex/` directory and in the `~/.iex` file itself add:
```
Code.compiler_options(ignore_module_conflict: true)
Code.compile_file("~/.iex/iex_watch_tests.exs", File.cwd!())
unless GenServer.whereis(IExWatchTests) do
{:ok, pid} = IExWatchTests.start_link()
# Process will not exit when the iex goes out
Process.unlink(pid)
end
import IExWatchTests.Helpers
```
To call `iex` just do:
```
ELIXIR_ERL_OPTIONS="-pa $HOME/.iex" MIX_ENV=test iex -S mix
```
The `IExWatchTests.Helpers` allows to call `f` and `s` and `a`
to run failed, stale and all tests respectively.
You can call `w` to watch tests and `uw` to unwatch.
Theres is a really simple throttle mecanism that disallow run the suite concurrently.
"""
defmodule Helpers do
defdelegate a, to: IExWatchTests, as: :run_all_tests
defdelegate f, to: IExWatchTests, as: :run_failed_tests
defdelegate s, to: IExWatchTests, as: :run_stale_tests
defdelegate w, to: IExWatchTests, as: :watch_tests
defdelegate uw, to: IExWatchTests, as: :unwatch_tests
end
defmodule Observer do
use GenServer
@impl true
def init(opts) do
{:ok, opts}
end
@impl true
def handle_cast({:suite_finished, _times_us}, config) do
IExWatchTests.unlock()
{:noreply, config}
end
@impl true
def handle_cast(_, config) do
{:noreply, config}
end
end
def start_link do
GenServer.start_link(__MODULE__, %{watcher: nil, lock: false}, name: __MODULE__)
end
def watch_tests do
GenServer.cast(__MODULE__, :watch_tests)
end
def unwatch_tests do
GenServer.cast(__MODULE__, :unwatch_tests)
end
def run_all_tests do
GenServer.call(__MODULE__, {:run, :all}, :infinity)
end
def run_failed_tests do
GenServer.call(__MODULE__, {:run, :failed}, :infinity)
end
def run_stale_tests do
GenServer.call(__MODULE__, {:run, :stale}, :infinity)
end
def unlock do
GenServer.cast(__MODULE__, :unlock)
end
@impl true
def init(state) do
Process.flag(:trap_exit, true)
ExUnit.start(autorun: false, formatters: [ExUnit.CLIFormatter, IExWatchTests.Observer])
{:ok, state}
end
@impl true
def handle_cast(:watch_tests, state) do
{:ok, pid} =
Task.start(fn ->
cmd = "fswatch lib test"
port = Port.open({:spawn, cmd}, [:binary, :exit_status])
watch_loop(port)
end)
{:noreply, %{state | watcher: pid}}
end
@impl true
def handle_cast(:unwatch_tests, %{watcher: pid} = state) do
if is_nil(pid) or not Process.alive?(pid) do
IO.puts("Watcher not running!")
else
Process.exit(pid, :kill)
end
{:noreply, %{state | watcher: nil}}
end
@impl true
def handle_cast({:run, mode}, %{lock: false} = state) do
do_run_tests(mode)
{:noreply, %{state | lock: true}}
end
@impl true
def handle_cast({:run, _mode}, %{lock: true} = state) do
{:noreply, state}
end
@impl true
def handle_cast(:unlock, state) do
{:noreply, %{state | lock: false}}
end
@impl true
def handle_call({:run, _mode}, _from, %{lock: true} = state) do
{:reply, :locked, state}
end
@impl true
def handle_call({:run, mode}, _from, %{lock: false} = state) do
do_run_tests(mode)
{:reply, :ok, %{state | lock: true}}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
defp watch_loop(port) do
receive do
{^port, {:data, _msg}} ->
GenServer.cast(__MODULE__, {:run, :stale})
watch_loop(port)
end
end
defp do_run_tests(mode) do
IEx.Helpers.recompile()
# Reset config
ExUnit.configure(
exclude: [],
include: [],
only_test_ids: nil
)
Code.required_files()
|> Enum.filter(&String.ends_with?(&1, "_test.exs"))
|> Code.unrequire_files()
args =
case mode do
:all ->
[]
:failed ->
["--failed"]
:stale ->
["--stale"]
end
result =
ExUnit.CaptureIO.capture_io(fn ->
Mix.Tasks.Test.run(args)
end)
if result =~ ~r/No stale tests/ or
result =~ ~r/There are no tests to run/ do
IExWatchTests.unlock()
end
IO.puts(result)
end
end

There is an explanition about how to use it on the @moduledoc, but briefly you just need to call iex prepending the ~/.iex directory on the Elixir path:

  ELIXIR_ERL_OPTIONS="-pa $HOME/.iex" MIX_ENV=test iex -S mix
Enter fullscreen mode Exit fullscreen mode

The IExWatchTests.Helpers that is imported on ~/.iex.exs allows to call f and s and a to run failed, stale and all tests respectively.

You can call w to watch tests and uw to unwatch.

There is a really simple throttle mechanism that disallow run the suite concurrently.

That is all. Enjoy!

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (4)

Collapse
 
alexandremcosta profile image
Alexandre Marangoni Costa

Nice tip! Any idea on how to make Mix.Tasks.Test.run([]) to work on umbrella projects? Mine always output There are no tests to run

Collapse
 
marciol profile image
Marcio Lopes de Faria

I need to figure out, but I think that must be simple.

Collapse
 
alexandremcosta profile image

For some reason, I also can't import IExWatchTests.Helpers from .iex.exs

$ ELIXIR_ERL_OPTIONS="-pa $HOME/.iex" MIX_ENV=test iex -S mix
Erlang/OTP 24 [erts-12.0.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]
Interactive Elixir (1.12.1) - press Ctrl+C to exit (type h() ENTER for help)
Error while evaluating: /Users/xxx/.iex.exs
** (CompileError) /Users/xxx/.iex.exs:11: module IExWatchTests.Helpers is not loaded and could not be found
Enter fullscreen mode Exit fullscreen mode

But if I comment this line and run it from iex, it works!

Thread Thread
 
marciol profile image
Marcio Lopes de Faria

Yes, I wrote another post explaining why I concluded that this approach is less than ideal, because there is a lot of limitation on what you can do in .iex.exs. For example, there is a IEx.Helpers.import_if_available/2 to deal with it.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay