2. Hello, World

Let’s continue with tradition and start with a program that prints out "Hello, World" to console.

Create a new directory, learn-elixir-with-tests/ to contain the code we will work through throughout this book. Within that directory, create a new hello-world/ directory to contain the code for this chapter.

In hello-world/, create a file named hello.exs file, typing in the following code:

Listing 1. hello-world/hello.exs
IO.puts("Hello, World")

After saving the file, you can run it in your terminal with elixir hello.exs, and see the expected output.

Our minimal code uses a call to IO.puts to print out a string to standard output, which in turn is written onto the terminal screen by your shell.

2.1. How to test

Our code works as expected, but how do we test it? As it stands, our code does two things: creates a string and prints it to standard output. The latter is hard to test, but we can separate out the former to make that part testable. Generally, its a good patten to separate out the core "domain" or "business logic" code out from its external "side effects" (in this case, printing to standard output) so that it is easy to test in isolation.

So let’s separate the creation of the string from the printing of it. Modify hello.exs to contain instead the following code:

Listing 2. hello-world/hello.exs
defmodule HelloWorld do
  def greet do
    "Hello, World"
  end
end

IO.puts(HelloWorld.greet())

Here we use defmodule to create a new module. In Elixir, functions are organized into modules, similar to how methods are organized into classes for most object-oriented languages. In our new module, we define our hello function which returns the "Hello, World" string. We then call it at the bottom of our file.

You can run elixir hello.exs again, and verify that we are still getting the same expected output of "Hello, World" printed to our screen.

2.1.1. Writing our first test

With this refactored code, we can now create our first test. Create a new hello_test.exs file in the same directory, and add the following:

Listing 3. hello-world/hello_test.exs
Code.require_file("hello.exs")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end
end

In our test file, we use Code.require_file to load our HelloWorld definition from hello.exs. Before defining our test, we invoke ExUnit.start to start up ExUnit so it can run the tests we define.

Within our HelloWorldTest module, we use ExUnit.Case to "hook into" ExUnit. It creates the test macro that we use to define our test, which ExUnit then runs. And finally, in our test itself we assert that the string returned from our hello function is as expected. The assert macro will fail the test if its argument is not true.

What is a "macro"?

Macros are code that generate other code at compile time. They are an advanced concept we won’t cover until much later in the book. For now, think of them as a special progamming trick that Elixir uses to make some functionality easier for us to use.

2.1.2. Running our first test

You can run the test with elixir hello_test.exs. You should see output similar to:

$ elixir hello_test.exs
Hello, World
.

Finished in 0.06 seconds (0.06s on load, 0.00s on tests)
1 test, 0 failures

Randomized with seed 40368

The output shows how many tests were run and how many of those tests failed. As it runs it outputs a dot (which should be green if your terminal supports colors) for each successful test executed, in real time.

It also mentions a "randomized seed". Each time ExUnit runs, it executes the queued tests in a randomized order. That "seed" represents, in an indirect way, the specific order in which the tests ran this time, used for advanced test debugging. We won’t need to concern ourselves with this part of the output for any of the exercises in this book.

Lastly, the test output also has "Hello, World" at the top. This is because we never removed the IO.puts call at the end of our main hello.exs file. When we did Code.require_file in hello_test.exs to load our HelloWorld module, Elixir ran everything in the file. It ran the code within the defmodule block to define our module, but also that IO.puts statement, which is why "Hello, World" is printed out before our test even ran!

2.1.3. Cleanup: .exs and .ex

Right now our hello.exs file is trying to do two different things. First, it defines a HelloWorld module to encompass a testable and reusable way to greet the world. It’s trivially simple now, but we will be greatly expanding the functionality later in this chapter.

Secondly, hello.exs has a small "script" at the end that directly runs the code contained in our module.

Whenever we have two concerns like this, it is a good idea to separate them out. As we’ve seen, this way of structuring things has already lead to a minor problem around that print statement, IO.puts(HelloWorld.greet()) running during our tests, muddying our output. Other developers in the future may want to use our module and will hit a similar issue. Everytime they try to load our module, we’ll be printing out into their console. An undesirable side-effect, indeed!

In Elixir, we differentiate between files meant for "scripting" and files meant for "reusable libraries" by the file extension. .exs files are meant to be run directly, and are expected to have side-effects when run. .ex files are meant to be compiled and run, but to have no side-effects when they are initially loaded.

Let’s apply these principals by refactoring our implementation.

First, let’s rename hello.exs to hello.ex to make it clear its not a script file. Additionally, let’s remove the print statement from the file, too.

Listing 4. hello-world/hello.ex
defmodule HelloWorld do
  def greet do
    "Hello, World"
  end
end

Since we renamed our file, we need to update the import in our test, too. A test is meant to be invoked directly and is not meant to be re-used, so we will keep the .exs extension.

Listing 5. hello-world/hello_test.exs
Code.require_file("hello.ex")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end
end

Lastly, we will create a new script.exs file to contain our small little script. If we really want to print that beautiful "Hello, World" string to our console, we can run that directly!

Listing 6. hello-world/script.exs
Code.require_file("hello.ex")
IO.puts(HelloWorld.greet())

Nice! We can verify everything is still working by running our test and seeing the rogue "Hello, World" printing is gone:

$ elixir hello_test.exs
.

Finished in 0.03 seconds (0.03s on load, 0.00s on tests)
1 test, 0 failures

Randomized with seed 194747

And if we invoke our new script file directly, we can also see that things are working on that front, too:

$ elixir script.exs
Hello, World

With that out of the way, we can move onto making our HelloWorld even more featureful.

2.2. Hello, Reader

It’s time to make our module a bit more intimate. Let’s change greet to take in a string that specifies who to greet. If we call it with "Reader", it should respond "Hello, Reader".

2.2.1. TDD and arity

Let’s start with the test:

Listing 7. hello-world/hello_test.exs
Code.require_file("hello.ex")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end

  test "greets the noun that is passed in" do
    assert HelloWorld.greet("Reader") == "Hello, Reader"
  end
end

We’ve created a new test that asserts what we outlined above. Now, we’re going to run it. We run it even though we haven’t implemented the functionality to make sure our test is working. If it is, then it should fail. You would be surprised, but its not uncommon to write a test that doesn’t quite work, that always passes because the test itself was incorrect.

When you run it, you’ll see something like this:

$ elixir hello_test.exs
.

  1) test greets what is passed in (HelloWorldTest)
     hello_test.exs:11
     ** (UndefinedFunctionError) function HelloWorld.greet/1 is undefined or private
     code: assert HelloWorld.greet("Reader") == "Hello, Reader"
     stacktrace:
       HelloWorld.greet("Reader")
       hello_test.exs:12: (test)



Finished in 0.03 seconds (0.03s on load, 0.00s on tests)
2 tests, 1 failure

Randomized with seed 340990

We see one dot for our first test, which is still passing. Our new test fails because our greet function is not implemented. Specifically, there is no HelloWorld.greet/1 implemented. In Elixir, a function definition consists of two parts: the name and the number of parameters, or arity. The function we do have implemented is actually HelloWorld.greet/0, and that is the one our first test uses with great success.

2.2.2. String concat operator (<>)

Let’s try fixing the error and getting the test to pass by adding a new greet/1 function.

Listing 8. hello-world/hello.ex
defmodule HelloWorld do
  def greet do
    "Hello, World"
  end

  def greet(noun) do
    "Hello, " <> noun
  end
end

The string concatenation operator in Elixir is <>. More specifically, it is the binary concatenation operator. All strings in Elixir are represented by a contiguous sequence of bytes known as binaries. If that sounds obscure, don’t worry - we’ll dig more into it in a later chapter. For now, just know that we can append two binaries with the <> operator, and strings are technically binaries, so we can use it to append two strings, too.

If we run elixir hello_test.exs again, we will see that all tests are passing. Sweet!

2.2.3. Default parameter values

Now that our tests are passing, we can look to see if there is anyway to make our code a bit more elegant or simple. The tests we have allows us to change things around with confidence, as we know that any mistake we may make will be caught by them.

In both greet functions, we have a "Hello" string that is shared. That indicates to us that there is some degree of duplication across the two functions that we can eliminate. The functions are of course quite similar, the only difference being that greet/0 has a hard-coded noun of "World". In fact, we can make that more clear by changing the code like so, to leverage greet/1 and thus eliminate the duplication of the "Hello" string:

Listing 9. hello-world/hello.ex
defmodule HelloWorld do
  def greet do
    greet("World")
  end

  def greet(noun) do
    "Hello, " <> noun
  end
end

This pattern of having lower-arity functions that pass along certain values to call higher-arity functions with is quite common in Elixir. It’s so common that Elixir provides a special operator to do it more succinctly, the \\ default argument operator.

Update your module with the code below to see it in action:

Listing 10. hello-world/hello.ex
defmodule HelloWorld do
  def greet(noun \\ "World") do
    "Hello, " <> noun
  end
end

The Elixir compiler under the hood will do exactly what we just did manually: define a greet/0 function that calls greet/1 with the default value of "World".

With our refactor done, run your tests once more to verify everything is working. This is what TDD empowers us to do: pursue elegance with confidence, knowing that the tests will keep us within safe bounds.

2.2.4. The TDD cycle

With this change, we’ve seen how the Test-Driven Development (TDD) cycle works. It’s pretty simple:

  1. Write the test first; verify that is runs and fails correctly.

  2. Write the minimal amount of code that makes the test pass.

  3. Refactor the code, leveraging the tests to ensure the code still works.

This process is sometimes referred to more simply as "red, green, blue". Red to represent the usual color of the failing test in the terminal, green to represent the color of the passing test, and blue to arbitrarily represent the refactoring step.

Throughout this book, we will use this cycle as we explore new Elixir concepts. You may encounter many who propose that this sort of TDD cycle should be used for all code you create. I’m not personally so dogmatic: I mainly use TDD here because I think it is a great way to learn a new language, as the tight feedback cycle familiarizes one with invoking ones code and the sort of runtime and compiler errors the language has. I like to think of TDD as another tool in a software developer’s toolkit, and by the end of this book you will be well on your way to mastering the style.

2.3. Bonjour, monsieur

Right now our code only says "hello" in English. Let’s make it so that it can greet us in other languages, too. We will modify our greet function to additionally take in a language parameter, and use that to determine which greeting to use.

Let’s start, as always, with a test:

Listing 11. hello-world/hello_test.exs
Code.require_file("hello.ex")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end

  test "greets the noun that is passed in" do
    assert HelloWorld.greet("Reader") == "Hello, Reader"
  end

  test "greets the noun in French" do
    assert HelloWorld.greet("monsieur", "French") == "Bonjour, monsieur"
  end
end

We’ve created a test that calls a yet-to-be-defined HelloWorld.greet/2 function, passing in the desired greeting language as the second parameter. When we run the test to verify it is failing correctly, we see a familiar error about no greet/2 function defined:

1) test greets the noun in French (HelloWorldTest)
    hello_test.exs:15
    ** (UndefinedFunctionError) function HelloWorld.greet/2 is undefined or private. Did you mean one of:

          * greet/0
          * greet/1

This UndefinedFunctionError is exactly what we would expect, since we have not defined the function. It is always useful to verify that the test not only fails, but fails in the way you expect it to, as it does here. By getting to know the various types of compile-time and runtime failures Elixir has, and what they look like, we build up a deeper understanding of the language itself. In programming, knowing why something doesn’t work is very close to knowing what needs to be changed to make it work.

2.3.1. Flow control with if and else

Let’s make the change to fix our failing test. We need to add a new function greet/2 that takes in our new language parameter. That paramater needs to default to "English" if not explicitly provided, so our existing tests keep on working as before.

Listing 12. hello-world/hello.ex
defmodule HelloWorld do
  def greet(noun \\ "World", language \\ "English") do
    if language == "French" do
      "Bonjour, " <> noun
    else
      "Hello, " <> noun
    end
  end
end

Here we use if and else to determine what greeting to use. The statement next to the if is evaluated, and if it evaluates to a truthy value, the block underneath it is executed. If the statement evaluates to a falsy value, then the else block is executed instead.

If we run our tests again, we will see they are all passing.

$ elixir hello_test.exs
...

Finished in 0.07 seconds (0.06s on load, 0.00s async, 0.01s sync)
3 tests, 0 failures

Randomized with seed 432929

2.4. Hola, mundo

Two languages is cool and all, but let’s finish it up by supporting a third: Spanish.

Let’s add a test. It’ll be pretty similar to the previous one:

Listing 13. hello-world/hello_test.exs
Code.require_file("hello.ex")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end

  test "greets the noun that is passed in" do
    assert HelloWorld.greet("Reader") == "Hello, Reader"
  end

  test "greets the noun in French" do
    assert HelloWorld.greet("monsieur", "French") == "Bonjour, monsieur"
  end

  test "greets the noun in Spanish" do
    assert HelloWorld.greet("mundo", "Spanish") == "Hola, mundo"
  end
end

If you run the tests, you will see it fail with a nicely formatted error:

1) test greets the noun in Spanish (HelloWorldTest)
    hello_test.exs:19
    Assertion with == failed
    code:  assert HelloWorld.greet("mundo", "Spanish") == "Hola, mundo"
    left:  "Hello, mundo"
    right: "Hola, mundo"
    stacktrace:
      hello_test.exs:20: (test)

The error message says the code on the left side of the == operator evaluates to "Hello, mundo", while right side says it should be "Hola, mundo". For this error, the issue is not having a function with the correct arity, but instead our existing greet/2 function not providing the correct behavior.

2.4.1. Handling cases with case

To get our test to pass, we need to handle this third case in our existing code. In addition to if and else for when you have one or two different cases to handle in your code, Elixir also has the conveniently named case expression for handling any number of cases.

Here we can see it in action:

Listing 14. hello-world/hello.ex
defmodule HelloWorld do
  def greet(noun \\ "World", language \\ "English") do
    case language do
      "French" ->
        "Bonjour, " <> noun

      "Spanish" ->
        "Hola, " <> noun

      "English" ->
        "Hello, " <> noun
    end
  end
end

To use case, we provide a paramater and then define a series of clauses that could match the parameter. After each clause we have an arrow -> and then the code we want to execute if the parameter matches that clause.

If we run our tests, we see that everything is passing:

$ elixir hello_test.exs
....

Finished in 0.04 seconds (0.04s on load, 0.00s async, 0.00s sync)
4 tests, 0 failures

Randomized with seed 151267

2.4.2. Pattern matching with case

Our HelloWorld module is looking pretty good, but it currently has one major flaw: it doesn’t handle the case of an unknown language. If I call it with the "Gibberish" language, for example, it won’t know what to do!

Let’s update it to handle this case. As always, before we start adding any functionality, let us first add a test outlining what we want:

Listing 15. hello-world/hello_test.exs
Code.require_file("hello.ex")
ExUnit.start()

defmodule HelloWorldTest do
  use ExUnit.Case

  test "greets the world" do
    assert HelloWorld.greet() == "Hello, World"
  end

  test "greets the noun that is passed in" do
    assert HelloWorld.greet("Reader") == "Hello, Reader"
  end

  test "greets the noun in French" do
    assert HelloWorld.greet("monsieur", "French") == "Bonjour, monsieur"
  end

  test "greets the noun in Spanish" do
    assert HelloWorld.greet("mundo", "Spanish") == "Hola, mundo"
  end

  test "apologizes to the noun if unknown language" do
    assert HelloWorld.greet("Bob", "Gibberish") == "Sorry, Bob. I can't speak Gibberish!"
  end
end

If we run the test, we will see an error:

1) test greets the noun in English if unknown language (HelloWorldTest)
    hello_test.exs:23
    ** (CaseClauseError) no case clause matching: "Gibberish"
    code: assert HelloWorld.greet("Bob", "Gibberish") == "Sorry, Bob. I can't speak Gibberish!"
    stacktrace:
      hello.ex:3: HelloWorld.greet/2
      hello_test.exs:24: (test)

Here our test hit an error when executing. That CaseClauseError says that our case statement does not have a clause that matches the parameter, "Gibberish". What we want is a clause that matches any language parameter we may provide to the greet/2 function.

Let’s add it:

Listing 16. hello-world/hello.ex
defmodule HelloWorld do
  def greet(noun \\ "World", language \\ "English") do
    case language do
      "French" ->
        "Bonjour, " <> noun

      "Spanish" ->
        "Hola, " <> noun

      "English" ->
        "Hello, " <> noun

      lang ->
        "Sorry, " <> noun <> ". I can't speak " <> lang <> "!"
    end
  end
end

And if we run it, we see our tests pass:

$ elixir hello_test.exs
.....

Finished in 0.07 seconds (0.07s on load, 0.00s async, 0.00s sync)
5 tests, 0 failures

Randomized with seed 616791

For string and other constants, like "English", the match only succeeds if the value in the clause and the parameter match exactly. If the case clause contains a variable, however, it will match any parameter. Moreover, Elixir will assign the value of the matched parameter into the variable.

This also means that the clauses in the case block are matched from top to bottom. In fact, if we moved our lang → clause to the top, none of our other clauses would get matched, since our variable clause matches everything!

That may all seem like a bunch of "Gibberish" to you, but don’t worry! What I’ve described is the "pattern matching" that is at the core of Elixir. We will see it again and again in how other parts of Elixir works, and build up understanding slowly but surely.

2.5. Conclusion

Our HelloWorld module is done and fully functional. In fact, we also have a full suite of tests proving that it works just the way we want it to! Our code is elegant and succinct, as most Elixir code tends to be, while being clear to read. We know what it does and how it works decently well, and that understanding, if incomplete, will be filled in more over the coming chapters.

2.5.1. What we learned in this chapter

  • .exs files are for running directly, and often have "side effects", such as printing to console.

  • .ex files are for reusable modules, and usually do not have any "side effects".

  • In Elixir, functions are defined by their name and the number of parameters they have, or arity.

  • You can concatinate strings in Elixir with the <> operator.

  • TDD, or test-driven development, is the practice of writing the test before developing the implementation.

  • We can use if and else for handling one or two cases differently in our code.

  • With case, we can handle any number of cases with a series of different clauses.

  • With case, variables match any parameter, and are assigned the value of the parameter.

  • Pattern matching is at the core of much of Elixir, and it is how case works.

2.5.2. Exercises

Here are some additional exercises you can do. Each of these involve updating the test with the new requirements, then updating the implementation to make the tests pass. As always: start with the test!

  • Update greet to handle the "Esperanto" language. Hello in Esperanto is saluton.

  • Change the default noun from World to Space.

  • Change the default language from English to Esperanto.