Red Green Repeat Adventures of a Spec Driven Junkie

Intro to Elixir's Testing and Executables

I love learning a new programming language but I always hate reading through ‘tutorials’ or the basics stuff of how to do basic math in a language. Almost all languages have the same things…

Sure, there’s some stuff important topics, like what might be an array data structure in another language are tuples in elixir. But since I know and like to do Test Driven Development, I want to know to:

  • How to get tests going and
  • How to ship code?

Learning how to test code in a language early helps me set the project up and get a good workflow going. There’s nothing worse than going back to write tests on code. I like to dive in and learn by coding. A great way to learn is to have tests.

Understanding how to ship code in a language also determines the workflow.

Starting Tests in Elixir

The topic of testing doesn’t come up until the second book in the official elixir guides!

So I will jump the first book of the official elixir guides now and jump right into setting up tests in elixir.

After installing elixir (the iex prompt works), just run: $ mix test <a project name> this will make a new folder with the project name!

$ mix new project_name
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/project_name.ex
* creating test
* creating test/test_helper.exs
* creating test/project_name_test.exs

To run tests, use mix test

$ cd project_name
$ mix test
..

Finished in 0.08 seconds
2 tests, 0 failures

Randomized with seed 550547

Two Tests?

What the??? There’s already two tests and they’re passing? What’s in those files??

Let’s open the main test file, project_name/test/project_name_test.exs and examine the contents:

defmodule ProjectNameTest do
  use ExUnit.Case
  doctest ProjectName

  test "greets the world" do
    assert ProjectName.hello() == :world
  end
end

Test One: Unit

The first test looks like it’s a typical unit test and the ProjectName.hello() test is passing already because project_main.ex already has:

def hello do
  :world
end

This is good, but the output of mix test listed two tests, where is this second test coming from??

Test Two: DocTest

Looking at the test file again, this line gives a hint for where the second test is coming from:

doctest ProjectName

and looking at the header of the lib/project_name.ex file, this is the contents:

@moduledoc """
Documentation for ProjectName.
"""

@doc """
Hello world.

## Examples

    iex> ProjectName.hello
    :world

"""

The line: iex> ProjectName.hello is the second test! Changing the comment to:

iex> ProjectName.hello
:computer

and running tests again:

$ mix test
Compiling 1 file (.ex)
.

  1) test doc at ProjectName.hello/0 (1) (ProjectNameTest)
     test/project_name_test.exs:3
     Doctest failed
     code: ProjectName.hello === :computer
     left: :world
     stacktrace:
       lib/project_name.ex:11: ProjectName (module)



Finished in 0.09 seconds
2 tests, 1 failure

Randomized with seed 115616

Causes a failing test, even though the change was in the comments section of the file, not the test file.

So, wow, mix tests runs all tests, those found in the tests directory AND those found in the documentation.

This style of using the documentation as a test is similar to Python’s doctest system.

Retro: Tests in Elixir

So that’s how tests work. This is better than any other system I have worked on, including Ruby/Minitest/Rspec. Going from zero to two different testing systems with one built in command of the programming language.

Building Executables

The topic of creating a binary to deploy code is covered as an advanced topic, which might be three books in!

Elixir can compile binaries for distribution by configuring the build file: mix.exs in the root of the project folders.

The generated file has these contents:

defmodule ProjectName.Mixfile do
  use Mix.Project

  def project do
    [
      app: :project_name,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
    ]
  end
end

For executables, Elixir requires an escript option in the project and that has to point to the whole project’s main function. It can exist in any module, but the function name must be main. Also, do not set the escript main option to the module and name, like: ProjectName.main.

So, to add the escript option in the current project created earlier, add main function to the project_name.ex file and its contents would be:

defmodule ProjectName do
  @moduledoc """
  Documentation for ProjectName.
  """

  @doc """
  Hello world.

  ## Examples

      iex> ProjectName.hello
      :computer

  """
  def hello do
    :world
  end

  def main(args \\ []) do
    IO.puts "Hello from elixir"
  end
end

The escript entry in the mix.exs file would be:

def escript do
  [main_module: ProjectName]
end

and adding an entry in the project section of the file:

escript: escript()  

Making the whole mix.exs file to be:

defmodule ProjectName.Mixfile do
  use Mix.Project

  def project do
    [
      app: :project_name,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(),
      escript: escript()
    ]
  end

  def escript do
    [main_module: ProjectName]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
    ]
  end
end

To build the executable, run: $ mix escript.build

$ mix escript.build
Compiling 1 file (.ex)
warning: variable "args" is unused
  lib/project_name.ex:19

Generated escript project_name with MIX_ENV=dev

To run the binary, execute it like any UNIX command: ./project_name

$ ./project_name
Hello from elixir
$

Retro: Shipping Elixir Code

It’s nice to have a way to ship the code that is not just the source code. I like having the main functionality as well. I miss that from C where there is a definite place to start looking for program flow.

Conclusion

This is just the basics with elixir that I like to get started with: understand how to get tests started and understanding how to deploy code. This is important to me as I like to have tests around my code, even when learning.

I have been really stuck at code retreats where most of the time is setting up just the testing framework, even for a language that has testing at its core. I really appreciate the simplicity of elixir’s approach, even with DocTests!

Building out executables is a nice change from languages where deploying code means deploying the whole source.

I’m looking forward to spending more time in elixir, reading over the first book I skipped over!