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
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.