Best practices for testing with doctest

Hi Guys, this is my fist post, and first delve into the elixir language. I am in process of reading Dave Thomas’ book and was doing the exercises like a real nice boy would, and I got interested in Elixir’s doctest feature (which is not in the book’s chapter I am in, btw). But I ran into the following issue:

Some of my methods, just print stuff to the console, IO.puts. When I wrote regular tests, on the test file, using ExUnit.{Case, CaptureIO}, I would simply do something like:

  test "Basic game output" do
    assert capture_io(fn ->
             guess(273, 1..1000)
           end) ==
             "Is it 500?\nIs it 250?\nIs it 375?\nIs it 312?\nIs it 281?\nIs it 265?\nIs it 273?\n273\n"
  end

So my first question, is: How could I doctest a function that does an IO.puts, instead of actually returning a value?

Perhaps the answer will be: use both styles, doctest for methods that return values and ExUnit.CaptureIO for methods that print to the console. What is the take on this?

Another question would be: how can I test methods that may result in System.halt/1 ? This was breaking my test flow. What I have done so far, is extracted a my_halt/1 method that simply calls System.halt(code) when env is not :test

My recommendation, especially if you have a lot of these methods is to separate out your side-effects from your business logic in a similar way that any/most of the functions that you create with Ecto actually create/operate on a Changeset rather than manipulating the database directly (Plug operates similarly). So your guess/2 function instead of looking something like this:

def guess(number, range) do
  IO.puts "Is it 500?\nIs it 250?\nIs it 375?\nIs it 312?\nIs it 281?\nIs it 265?\nIs it 273?\n273\n"
end

Would look more like this:

def guess(number, range) do
  %{
    output: "Is it 500?\nIs it 250?\nIs it 375?\nIs it 312?\nIs it 281?\nIs it 265?\nIs it 273?\n273\n"
  }
end

Which you could then test like:

test "Basic game output" do
  assert guess(273, 1..1000) ==
    %{output: "Is it 500?\nIs it 250?\nIs it 375?\nIs it 312?\nIs it 281?\nIs it 265?\nIs it 273?\n273\n"}
end

This will make a doctest easier to write.

Of course you do need to put your side-effects somewhere so you can have a simple function such as:

def send_output_to_terminal(%{output: output}) do
  IO.puts(output)
end

And the send_output_to_terminal function is of course very easy to test. One benefit of this decoupling is that if you now want to create a new interface to your application (for example over a Phoenix Socket) you can keep all your business logic the same, and just write a send_output_to_socket function.

To provide better structure rather than operating one a map you can create a simple Struct to represent the results of your business/game logic. You can also add a key to represent if you should run System.halt/1 or not.

I recommend this article all the time, but in case you haven’t seen it I’d highly recommend this article: http://theerlangelist.com/article/spawn_or_not

While it is primarily about processes, it contains a good example of this type of decoupling of side-effects from business logic.

If you have the time and resources, PragDave’s course is also excellent: Elixir for Programmers (PragDave) (Currently on offer for $30!)

3 Likes

Great! Thanks so much for taking the time to enlighten me. I will both read the article and check out Dave’s course. Though I am more inclined at the moment to read Ulisses’ new book on Elixir.

2 Likes