Your first TDD Elixir project

by Enrique Cerda

In my goal to learn Elixir, I decided to work on a beginner’s StringCalc kata. I read about test-driven development on Elixir and gladly found out that Elixir comes with its own testing framework called ExUnit. Elixir too has its own command line shell called iex. I found out to be a helpful tool to dynamically try and test some commands. Moreover, Elixir too comes with a dependency and project management tool called Mix.

So first things first, create your project. Mix will help us with this:

$ mix nex stringcalc

Mix creates a new project structure including a README and of course our tests, including its own folder. This is the folder where you’ll write each new module test. The testing file must be named as your tested file, followed by an underscore and the word “test”. So for “stringcalc.ex” you should have a file named “stringcalc_test.exs”. Your current testing file has one test:

# "stringcalc_test.exs"

defmodule StringcalcTest do
  use ExUnit.Case
  doctest Stringcalc

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

Run this simple test with mix test and your assertion should be True, outputting 0 failures:

1 doctest, 1 test, 0 failures

So TDD starts with the simplest scenario you could imagine. For this, I wanted to have an “add” method that gave me 0 when no parameter was passed. Writing test first:

# "stringcalc_test.exs"

test "it returns zero when empty string is entered" do
  assert Stringcalc.add() == 0
end

After this, I run my mix test command. But a more dynamic approach, and following XP, I had my IDE run it automatically on each save. This will obviously fail since I don’t have an add method:

$ mix test

1) test returns 0 when no input given (StringcalcTest)
     test/stringcalc_test.exs:9
     ** (UndefinedFunctionError) function Stringcalc.add/0 is undefined or private
     code: assert Stringcalc.add() == 0
     stacktrace:
       (stringcalc) Stringcalc.add()
       test/stringcalc_test.exs:10: (test)

Note the message: Stringcalc.add/0 is undefined or private. Is it undefined or private? Following this error, since no add/0 method exists, I proceed and write it inside our Stringcalc module:

# stringcalc_test.exs

def add() do
  0
end

And now finally I can test again with mix test and confirm that my test is now passing:

1 doctest, 1 test, 0 failures

TDD is about testing your desired functionality, thus creating it in the process in a test-driven fashion. It took some practice for me to make this intuitive. Following these tests will ask for the next desired functionality, and reading your console output will guide you through. So in my case I wanted my stringcalc to be able to add two string-comma separated digits. Take a look at my final outcome and see how I went from the simplest case-scenario to the final one. At the end, tests will ensure that any changes or modifications will not break your code and if they do, your output will point you to where is the code failing, making it easy to change and detect.

# stringcalc.ex

defmodule Stringcalc do
  def add(string) do
    input = String.new(string)
    return 0 if input.nil?
  end

  def add("//" <> string) do
    newline_position = string |> to_charlist() |> Enum.find_index(&(&1 == ?\n))
    delimiter = String.slice(string, 0..(newline_position - 1))
    string = String.slice(string, (String.length(delimiter) + 1)..String.length(string))

    add(string, delimiter)
  end

  def add(string, delimiter \\ ",") do
    string = String.replace(string, "\n", delimiter)
    string = String.split(string, delimiter)
    result = Enum.reduce(string, 0, fn x, acc -> handle_number(x) + acc end)
  end

  defp handle_number(string) do
    number = String.to_integer(string)

    if number < 0 do
      raise NegativeError, message: "No negatives allowed: " <> Integer.to_string(number)
    end

    number
  end
end

defmodule NegativeError do
  defexception message: "No negatives allowed"
end
# stringcalc_test.exs

defmodule StringcalcTest do
  use ExUnit.Case
  doctest Stringcalc

  import Stringcalc

  test "it returns zero when empty string is entered" do
    assert add("") == 0
  end

  test "it returns sum of two string numbers" do
    assert add("1,2") == 3
  end

  test "it returns sum of more than two string numbers" do
    assert add("1,2,4,5,1") == 13
  end

  test "it allows new lines as separators instead of commas" do
    assert add("1,2\n3\n3") == 9
  end

  test "it supports different delimiters" do
    assert add("//;\n1;2;4") == 7
  end

  test "it raises NegativeError if negatie number in list" do
    assert_raise NegativeError, "No negatives allowed: -2", fn -> Stringcalc.add("1,-2") end
  end
end

Conclusion

Test-driven development will not only help your code be safe against future modifications or protected against bugs, it will guide you through and point you in the right direction. In my case, I didn’t really know much about Elixir but message outputs from my TDD process guided me and explicitly told me the errors that finally made my tests pass.