Unit testing in Elixir

It's been a few months now that we've been toying with Elixir here at OmbuLabs. Particularly with Phoenix. However, I like to toy a bit with the language without the added complexity of a framework on it, in order to better understand it, how it works and how things are done.

It took me a while, but as I read through "Programming Phoenix", I had the idea to implement a "simple" calculator program in Elixir. The reasons were quite simple:

  • I wanted to better grasp unit testing with ExUnit, and a calculator has very clear specifications, making it the ideal candidate.
  • I also wanted to put a little twist to things and while math has very clear specs, it can have some very complex operations, so I decided to make my calculator do derivations.

For those of you that aren't versed in calculus, don't worry, I'll give just what you need to know in order to understand what I'm doing, but trust me, you won't need to really understand calculus to understand the code.

Setup

First and foremost, the idea is to use TDD and get to know ExUnit better. ExUnit is simple, as per the elixir's team description in the contribution section of their repo, which appeals a lot to me. I like RSpec as much as the next dev, but simple sounds like music to my ears. So what a better way to practice TDD than with a simple framework?

Our first order of business is to implement the 4 basic operations. After all, not much use in a calculator that can't sum. And that's where we'll start.

First, we'll create our mix project. Mix is a build tool for creating, compiling and testing Elixir projects that comes with Elixir. Read the docs):

mix new calculator_3000

This will generate our mix project with a simple Hello World program. Let's go straight to the calculator_3000_test.exs file. This is what we'll find:

defmodule Calculator3000Test do
  use ExUnit.Case
  doctest Calculator3000

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

Let us remove that test over there. Now we start.

The easy part

ExUnit is very simple, but still quite similar to RSpec in that we create block to describe a function and then add the specific tests. In our case, we want to describe sum. Literally:

describe "sum" do
end

Not, when we call sum we expect, naturally, that the sum is performed correctly. Therefore:

describe "sum" do
  test "sums 2 integers correctly" do
    assert Calculator3000.sum(2, 3) == 5
  end
end

As you might've guessed, in ExUnit, all tests to be run are marked as a call to the test function, that takes a string to identify the test and the block to be executed. In our case, we call the assert function to verify that by calling Calculator3000.sum/2 with the numbers 2 and 3, it returns a 5.

If we run mix test now, we get:

calculator_3000 on  trunk [?] is 📦 v0.1.0 via 💧 
❯ mix test
warning: Calculator3000.sum/2 is undefined or private
  test/calculator3000_test.exs:7: Calculator3000Test."test sum sums 2 integers correctly"/1



  1) test sum sums 2 integers correctly (Calculator3000Test)
     test/calculator3000_test.exs:6
     ** (UndefinedFunctionError) function Calculator3000.sum/2 is undefined or private
     code: assert Calculator3000.sum(2, 3) == 5
     stacktrace:
       (calculator_3000 0.1.0) Calculator3000.sum(2, 3)
       test/calculator3000_test.exs:7: (test)



Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 test, 1 failure

Randomized with seed 495963

So now we go for the implementation. One very interesting thing about Elixir is that it heavily supports documentation. You can mark a function using the @doc attribute and Elixir will generate documentation for you (and test it too!). Check out this page if you want to know more. Let's try that out.

Naturally, Elixir implements summation for us, so we have no extra work, however, we can still think of what a sum should do. At the most basic level we expect that if we give two numbers to a sum function, it will give us the result of that sum. Putting it directly:

@doc """
Sum operation.

## Examples
    iex> Calculator3000.sum(2, 3)
    5
"""

Simple, right? Notice how I added the 4 spaces and the iex> prompt in the documentation string. That's what tells Elixir that it should run that, to make sure you keep them up to date. You can pretty much just write out what you'd see in IEx and it will work.

Now we just need to translate that to a function. Which, naturally, is as simple as can get. We just want to grab the numbers given as arguments and sum them using the + operator given to us by Elixir:

@doc """
Sum operation.

## Examples
    iex> Calculator3000.sum(2, 3)
    5
"""

def sum(num1, num2) do
  num1 + num2
end

All done. Now all we need to do is run the test and see what goes wrong:

calculator_3000 on  trunk [?] is 📦 v0.1.0 via 💧 
❯ mix test
Compiling 1 file (.ex)
warning: module attribute @rad_in_deg was set but never used
  lib/calculator3000.ex:24

warning: module attribute @max_value was set but never used
  lib/calculator3000.ex:11

warning: module attribute @epsilon was set but never used
  lib/calculator3000.ex:8

..

Finished in 0.03 seconds (0.00s async, 0.03s sync)
1 doctest, 1 test, 0 failures

Randomized with seed 452131

Notice that the summary says 1 doctest, 1 test, 0 failures. That's the test task testing your docs as well as your code. Just to show that it does break, if I change, say, the number of args in the docs but not in the function, it will complain:

  @doc """
  Sum operation.

  ## Examples

      iex> Calculator3000.sum(2, 3, 5)
      5

  """

  def sum(num1, num2) do
    num1 + num2
  end

mix test output:

  1) doctest Calculator3000.sum/2 (1) (Calculator3000Test)
     test/calculator3000_test.exs:3
     ** (UndefinedFunctionError) function Calculator3000.sum/3 is undefined or private. Did you mean:

           * sum/2

     stacktrace:
       (calculator_3000 0.1.0) Calculator3000.sum(2, 3, 5)
       (for doctest at) lib/calculator3000.ex:37: (test)

.

Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 doctest, 1 test, 1 failure

Randomized with seed 510432

Notice how the failure output indicates it as a doctest and not a regular test.

The procedure for multiplication is the same for summation: there aren't any special cases that we really care about. For now.

More intersting cases

Subtraction and division are more interesting, however.

  • For subtraction we want to make sure that we get negative numbers should we do, say, Calculator3000.subtract(3, 2) (read it as "subtract 3 of 2").
  • For division we have the classic special case of division by zero, in which case we want our function to raise an error. We also only want precision to be of only one decimal place. Just because.

The choice of API for the division operation might be questionable. After all, it's not nice to throw errors at our users. Maybe later we can explore a convention as to what our methods return, so that we always have a return type the user can work with rather than have to handle exceptions. For now, let's write our tests:

  describe "subtraction" do
    test "subtracts a smaller number from a larger number" do
      assert Calculator3000.subtract(3, 5) == 2
    end

    test "subtracts a larger number from a smaller number" do
      assert Calculator3000.subtract(5, 3) == -2
    end
  end

When we run this, naturally, it fails:

  1) test subtraction subtracts a smaller number from a larger number (Calculator3000Test)
     test/calculator3000_test.exs:12
     ** (UndefinedFunctionError) function Calculator3000.subtract/2 is undefined or private
     code: assert Calculator3000.subtract(3, 5) == 2
     stacktrace:
       (calculator_3000 0.1.0) Calculator3000.subtract(3, 5)
       test/calculator3000_test.exs:13: (test)

.

  2) test subtraction subtracts a larger number from a smaller number (Calculator3000Test)
     test/calculator3000_test.exs:16
     ** (UndefinedFunctionError) function Calculator3000.subtract/2 is undefined or private
     code: assert Calculator3000.subtract(5, 3) == -2
     stacktrace:
       (calculator_3000 0.1.0) Calculator3000.subtract(5, 3)
       est/calculator3000_test.exs:17: (test)

.

Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 doctest, 3 tests, 2 failures

Randomized with seed 767685t

Again, it complains of a missing function. Let's add it:

  @doc """
  Subtract operation. In this case, the function should be understood
  as if one read: "subtract num1 of num2"

  ## Examples

    iex> Calculator3000.subtract(5, 3)
    -2
  """
  def subtract(num1, num2) do
    num2 - num2
  end

Again, simplest implementation possible. Not to mention that Elixir's - operator already deals with negative numbers, so that turns out to be a non issue. Running our tests we get:

.....

Finished in 0.03 seconds (0.00s async, 0.03s sync)
2 doctests, 3 tests, 0 failures

Randomized with seed 480728

All green.

Now, division. Taking into account our requirements above:

  describe "division" do
    # The one decimal place precision should work for both exact and inexact divisions
    test "exact division has one decimal place precision" do
      assert Calculator3000.divide(10, 2) == 5.0
    end

    test "inexact division has one decimal place precision" do
      assert Calculator3000.divide(10, 3) == 3.3
    end

    test "division by zero throws an ArithmeticError" do
      assert_raise ArithmeticError, fn ->
        Calculator3000.divide(10, 0)
      end
    end
  end

This time, if we merely delegate to the / operator, our tests fail:

  1) test division inexact division has one decimal place precision (Calculator3000Test)
     test/calculator3000_test.exs:31
     Assertion with == failed
     code:  assert Calculator3000.divide(10, 3) == 3.3
     left:  3.3333333333333335
     right: 3.3
     stacktrace:
       test/calculator3000_test.exs:32: (test)

....

Finished in 0.04 seconds (0.00s async, 0.04s sync)
3 doctests, 6 tests, 1 failure

Randomized with seed 49153

We must therefore operate on the result of calling / and restrict the precision of the float. Thankfully, the Float module has just the right method:

  @doc """
  Division operation.

  ## Examples

    iex> Calculator3000.divide(10, 2)
    5.0
  """
  def divide(num1, num2) do
    num1 / num2
    |> Float.round(1)
  end

So piping the result of num1 / num2 into Float.round/1 should get rid of that error:

.........

Finished in 0.04 seconds (0.00s async, 0.04s sync)
3 doctests, 6 tests, 0 failures

Randomized with seed 559569

And since we're not handling the ArithmeticError thrown by / if we try to divide by zero, it just gets thrown automatically and the requirement is fulfilled.

Finally, we get to the more interesting part of this article: a function that will calculate derivatives.

Derivation

So, first I must quickly explain what derivation is. In many sciences, it is useful to know how fast something is changing. So, for example, in physics, if we study how fast something is moving, we're studying that object's speed. Technically, we should call it velocity, but that distinction is not essential to understand derivation, so let's just go with speed. What matters is that we're clear that either speed or velocity are terms that mean the rate at which an object changes position.

However, because our method of studying motion is mainly interested in quantifying motion (i.e.: "yes, I know that you were moving, but how fast were you, sir?"), in order to lessen our work, it'd be mighty useful to be able to represent a motion using a simple mathematical function.

If you're not convinced, imagine having to write out a huge table with the position of an object and the times those positions were recorded in order to calculate an object's speed. It's gruesome, I know. Much better to just say that the position x of the stone I just threw is given by s(t) = x0 + v0*t + (g*t^2)/2, where s(t) will give us the position at any time, x0 is the starting point, which we can say is 0 right where we are, v0 is the starting speed, which in this case can be something like 5 m/s, t is the time, wich we can just say starts when the rock leaves our hand, and g is the acceleration due to the Earth's gravitational pull, wich is constant and equals -9.8 m/s^2.

At this point you're probably getting desperate. "What's with that negative acceleration? I've never seen this function in my life! Stop talking!".

Don't worry. This right now is that important part: The position of an object is given by some function s(t). If I want to also know the function that represents that objects speed, and, trust me, we want to know it, I'll I have to do is to derivate s(t), because the derivative is precisely the operation you can perform on a function that gives you it's rate of change. Recall that speed is the rate of change of an object's position, as explained above.

So now you say: "Yeah, thanks for nothing. Even if I did understand all that jibber jabber, how do I even calculate a derivative?" Easy peasy, my friend. If you want to know how fast something is going at a certain point, knowing that somthing's s(t), just plug it in this here equation:

v(t) = s(t + h) - s(t) / h, where h is puny. 0.0000001 will do.

Now we need to translate that into a test. For the sake of brevity, trust me on this one. I'm a scientist. At least I did graduate at this Scientist's School at my local uni:

  test "derivation" do
    function = fn x -> x ** 2 end
    assert Calculator3000.derive(function, 3) == 6.0
  end

What I'm stating here is that the value of the derivative of f(x) = x^2, written in elixir as x ** 2, at the point x = 3 is equal to 6. Don't believe me? Just plug it in the equation I gave earlier. Make sure to pick some tiny h like the one I gave. Use a calculator.

Our API is one I believe to be coherent: I want my derive function to grab whatever function you give it and calculate the derivative at the point you desire. Since I've not implemented the function yet, our test fails, so let's get to it.

  @doc """
  Derivation.

  This will calculate the derivative of a function f(x) at a
  certain value a according to the formula:

  f'(a) = lim(f(a + h) - f(a) / h), with h -> 0

  For Calculator3000 I picked h = 0.000000000001. Why? Yes.
  """
  def derive(function, value) do
  end

In the docs you get a more formal definition. I decided not to use the lim notation in my explanation just so as not to confuse those non-math savy. But it's the same idea as before. The difference is that if you solve that limit in the proper math way, you get a nice formula for the derivative. But that'd be way too complicated for this post.

Also, notice that I try to underscore the fact that h can be any value you want, but the smaller, the better. For practical human purposes, anything after 3 or 4 decimal places is overkill. But we use computers, so I'm going to make it worth my money by using as tiny an h I can think of.

So, to implement this function, we get to use a more advanced feature of elixir which is to pass a function as an argument and make it do it's thing without our function having to know what it is. This means that if I ever implement other mathematical functions like cosine, sine, hyperbolic tangent, Bessel's function or whatever other weird nonsense I can come up with, as long as the function argument returns some numeric value, our derive function will perform it's duties admirably. Except for a select few not so uncommon cases, like if a function diverges to infinity at a certain x, but let us not worry about those for now.

The way we call some function we have with a certain argument, in elixir is name_of_function.(argument)

Now, all we have to do is declare our h variable write out the derivation equation in a more readable fashion for programmers, and round that puppy to one decimal place, because that's how we roll:

  @doc """
  Derivation.

  This will calculate the derivative of a function f(x) at a
  certain value a according to the formula:

  f'(a) = lim(f(a + h) - f(a) / h), with h -> 0

  For Calculator3000 I picked h = 0.000000000001. Why? Yes.
  """
  def derive(function, value) do
    h = 0.000000000001
    numerator = function.(value + h) - function.(value)
    denominator = h

    numerator / denominator
    |> Float.round(1)
  end
end

Beautiful. All I do is separate the derivative in to the 2 parts of a division, the numerator and the denominator, just for clarity's sake, but I'm basically reproducing the equation for derivation. At the end I round it to one decimal point because that's all we care about.

Running our tests we finally get:

...........

Finished in 0.05 seconds (0.00s async, 0.05s sync)
4 doctests, 8 tests, 0 failures

Randomized with seed 251177

Our extra test is the multiplication test I omited in this article, since it's basically the same as the one for summation.

Conclusion

And that's it. Here I only went through the basics just to show simple assertions, doctesting and organization in ExUnit. And also to show off what we can call a "pro math move".

But there is more we can maybe add and things to watch out.

One of them are the divergent points of the function we pass to the derivation function. Derivatives can go to infinity. I doubt there's a computer that won't have issues with that.

Another thing to add are different math functions. After all, no use in having a calculator than can do derivatives if the most complex thing you can give it are polynomials. Maybe someday I'll try and add trigonometric functions and other things. Meanwhile, if you like this kind of stuff, there is a math lib for elixir worth checking out.

Finally, more on the programming side, our API can be better and maybe our tests don't have the best formulation in the world. In particular, standardizing what our functions return to our caller would be nice, especially if we'd like to build a more user friendly interface like a web, desktop or mobile app.

But these will be left for future articles.

The repo for this project can be found here