Things you could do with Mix

Tjaco oostdijk

Tjaco Oostdijk – 19 December 2018
824 words in about 4 minutes

Part 1: Creating Tasks

Recently I gave my very first talk at a conference, Code Beam Lite Amsterdam, and had a very great time doing so. So why not share my thoughts in the form of a blog post as well… The subject, Mix, is something that everybody that uses Elixir has definitely used. But you might have not discovered the power (and fun!) this little tool can bring to your workflow.

In this first part I’d like to start out simple. We’ll start by creating our very first mix task. Now, starting out with Hello World gets old pretty fast, so we’ll do something a little different. We’ll create a task that will show us the current time. I know, might also not be the greatest wonder in the world, because we could also just look at the time on our computer… But hey, do you want to learn how to create a mix task or not?

Ok, let’s get cracking! Most tutorials will show you to create the task itself as that is very straightforward, but let’s not do that. Let’s write a test first!

In order to write our first test, let’s use one of Mix’s best known tasks, mix new, to create our project.

1
mix new clock

After that we can create our test file:

1
2
mkdir -p test/mix/tasks
vim  test/mix/tasks/time_test.exs

And let’s create the boilerplate for our test:

1
2
3
4
5
6
7
8
9
defmodule Mix.Tasks.TimeTest do
  use ExUnit.Case, async: true

  describe "run/1" do
    test "it outputs the current time" do
      ...
    end
  end
end

If we look at the docs for Mix Tasks it has all the info we need to write our test right at the top:

A Mix task can be defined by simply using Mix.Task in a module starting with Mix.Tasks. and defining the run/1 function

Here is the boilerplate for our first function:

1
2
3
4
5
6
defmodule Mix.Tasks.Time do
  use Mix.Task

  def run(_argv) do
  end
end

Now it’s time to actually create a test that we can use. So what do we expect from this task? At first let’s start by just outputting the current time as a string to stdout.

In our tests it is not very useful to have the task output to stdout. We could of course use capture_io to catch stdout and test the output if it is what we expect. But I recently came across a nice blogpost by Jesse Cooke that points out a much nicer way to test shell output of mix.

So basically you can replace the shell that Mix uses with the current process with the Mix.shell/1 function. So we’ll do just that and put that at the top of our test_helper.exs

1
2
3
4
5
# Get Mix output sent to the current
# process to avoid polluting tests.
Mix.shell(Mix.Shell.Process)

ExUnit.start()

Once we do that we can use assert_received to test what ended up in the process mailbox. Nice!

I know I know, we’re getting there. Just a little more.

Here is the updated test:

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Mix.Tasks.TimeTest do
  use ExUnit.Case, async: true

  describe "run/1" do
    test "it outputs the current time" do
      Mix.Tasks.Time.run([])

      assert_received {:mix_shell, :info, [time]}

      assert time == # we need to define the output
    end
  end
end

So we use some nice pattern matching to catch the output of the command. Now the only thing we need is to define the output we expect. We don’t want to bring in any dependencies like Timex at this point, and getting the current local time is a little tricky with the current standard library. There is some nice stuff coming in 1.8, but that hasn’t landed just yet.

Luckily we can reach out to erlang to solve this problem for us and use the :calendar.local_time function. We can then use Elixir’s NaiveDateTime module to convert into a string very easily.

So this is our final test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defmodule Mix.Tasks.TimeTest do
  use ExUnit.Case, async: true

  describe run/1 do
    test it outputs the current time do
      Mix.Tasks.Time.run([])

      assert_received {:mix_shell, :info, [time]}

      current_time =
      :calendar.local_time
      |> NaiveDateTime.from_erl!
      |> NaiveDateTime.to_time
      |> to_string

      assert time == current_time
    end
  end
end

With that in place we can run our test and make it pass by adding this to our task. Note that this test against current_time only works when using seconds granularity. :) For this example that is good enough.

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Mix.Tasks.Time do
  use Mix.Task

  def run(_argv) do
    time =
    :calendar.local_time
    |> NaiveDateTime.from_erl!
    |> NaiveDateTime.to_time
    |> to_string

    Mix.shell.info(time)
  end
end

And now inside your project you can run mix time to output the current time to stdout :).

Now the task does not show up in the list of tasks when you run mix help. In the next part of this series we’ll explore how to do just that and also see if we can add some more features to our awesome clock.

You can find the repository for this code on github: https://github.com/drumusician/clock

Until next time!

At Kabisa, privacy is of the greatest importance. We think it is important that the data our visitors leave behind is handled with care. For example, you will not find tracking cookies from third parties such as Facebook, Hotjar or Hubspot on our website. Only cookies from Google and Vimeo are used in order to improve the user experience of our visitors. These cookies also ensure that relevant advertisements are displayed. Read more about the use of cookies in our privacy statement.