Mix: Test Watch Stale

Mix comes with a good set of tools, but it’s true power can be seen when we try to configure it. Mix makes it awfully easy to provide some basic configurations and overrides. One such configuration is aliases.

In this post, we’re going to use aliases to make our TDD lives easier while using a mix project.

$ mix test is the main task which runs all the tests for a mix project. It takes a command line option, --stale which runs only the tests which references the modules that changed since last $ mix test --stale call. You can read more about it here. It also accepts the option, --listen-on-stdin which listens on stdin while running the tests. When running tests with this option, tests are run again when it receives a newline. We can use these options in combination to give us the ability to run stale tests with just one keystroke: $ mix test --stale --listen-on-stdin.

Now, we want to integrate this with our file system, such that whenever we change “relevant” files, stale tests get run automatically without even a single keystroke.

To accomplish this we can write a custom alias in the mix.exs file:

This example is for an umbrella app, but can be modified for a non-umbrella too.

# in mix.exs

defmodule MyUmbrella.Mixfile do
  use Mix.Project

  def project do
    [
      apps_path: "apps",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases()
    ]
  end

  defp deps do
    []
  end

  defp aliases do
    [
      "test.watch.stale": &test_watch_stale/1,
    ]
  end

  # This function runs a system command with a watcher depending upon the
  # Operating System.
  def test_watch_stale(_) do
    System.cmd(
      "sh",
      ["-c", "#{get_system_watcher()} apps/ | mix test --stale --listen-on-stdin"],
      into: IO.stream(:stdio, :line)
    )
  end

  # Works only for Mac and Linux
  defp get_system_watcher do
    case System.cmd("uname", []) do
      {"Linux\n", 0} -> "inotifywait -e modify -e create -e delete -mr" # For Linux systems inotify should work
      {"Darwin\n", 0} -> "fswatch" # For Macs, fswatch comes directly installed
      {kernel, 0} -> raise "Watcher not supported on kernel: #{kernel}"
    end
  end
end

By defining a custom alias test.watch.stale and delegating it to the function, test_watch_stale/1, we are adding the ability to call $ mix test.watch.stale from the command line which calls the function test_watch_stale/1 with the list of arguments we pass from the command line (in this case none, so an empty list).

test_watch_stale function calls mix test --stale --listen-on-stdin piped from a watcher function which spits an stdin whenever an event is triggered, which in this case is when apps/ folder is modified.

get_system_watcher function gets the system watcher commands for different operating systems. Macs come preinstalled with fswatch, so this should work on a mac right away. Linux systems will have to install inotify-tools for this to work.

I decided not to support Windows for now, but there are file watcher binaries like watchman that work on all OS.

Now, we can follow the perfect TDD where our changes to the file system trigger relevant tests.

Test Watch Stale

TDD Heaven