Testing in Elixir:: Chapter 1: Introduction

Part 1

hoodsuphopeshigh
8 min readNov 26, 2017

Welcome to Chapter 1 in a series of blog posts related to testing in Elixir.

There are countless blogs that involve selling the dream of testing , so why not get some easy Medium claps by doing another?

So, why should you test? Aside from all the business benefits, the main benefit is that you as a developer will improve. It forces you to take a step back and think about what you’ve just written / are planning to write, and understand how the code works at a far deeper level.

In a world where Product are under pressure to ship everything ‘yesterday’, before Facebook buys them out; where a new competitor launches under your nose; where your new blockchain gets hacked or your ICO is found out to be nothing more than a pretty Powerpoint presentation — there is need for calm.

For a moment of peace and quiet.

For a second to repent and ask forgiveness for what you were just about to commit to master, because OBVIOUSLY there is no way in hell that was ever going to work…

git reset --hard HEAD

*CHANGE POINTS ALLOCATED TO STORY TO 8*

rm -rf /

*GOTO LUNCH*

The ability to sense check your assumptions about behaviour in a code base or with a language is key. Since languages do weird things, and inherited code bases even weirder things again, testing is a sanity check for yourself and the product as a whole.

So, why Elixir?

From personal experience, it is a lovely language to work with. It has had a lot of care and attention put into the design, resulting in a language that behaves how I would want a language to behave, as well as minimal “…the hell?!” moments. It also has a lot of depth to it, especially if you are coming to the language from Ruby and have been sold the dream of “It’s like Ruby, but functional and better”. This is perfectly fine, and you will get far and have a lot of fun using it this way. However, if you chip away at it and begin playing with the OTP parts, it’s insane how much more there is to it.

Finally, and most importantly, Elixir is a great language to test in — that is why we’re here, after all. It has a built in first-class library called ExUnit which will do the majority of what you need straight out of the box. For the obscure cases, it behaves nicely with external libraries.

Now, onto the crux of the article. We will start super basic, then build upon our knowledge with each chapter until we run out of things to learn how to test. Alternatively, we gaze into the OTP abyss long enough that it gazes back at us… Either/ or.

Hurrah, our first ticket.

A very nice start, so here we will take two numbers and output the result of this.

We will start by opening our terminal and running the command:

mix new chapter_1

This will create a whole scratch Elixir project for us, including setting up basic file structure and testing. It also handily provides us with a if this test fails... you have bigger problems test, which we can see by moving into the repo and running our test command:

cd chapter_1
mix test
Our first tests.

So, let’s have a look at what we have by default before solving our product problem. Inside our test/ directory we have a test_helper.exs and chapter_1_test.exs files.

The test_helper.exs file is used to set up the test framework and is required by mix before running our tests.

Before we inspect the chapter_1_test.exs , it is important to note that while standard Elixir files have a .ex file type, here we have .exs . The difference is that .ex files must be compiled before being run, which is important for code we will release. .exs files are script files, so are executed without compilation.

Inside chapter_1_test.exs we will see the following:

Our module name has the Test suffix which is a convention. Inside the module declaration we also have:

use ExUnit.Case :

This allows us to bring the ExUnit.Case module into the scope of our test module, which then uses an inbuilt callback to inject some code into the current context. This is needed to build our tests and run our assertions.

doctest Chapter1:

This is a type of test that we will cover shortly. It’s pretty awesome.

An actual test:

The structure of every test will look roughly like this:

You have the declaration at the top test "greets the world" . This is where you will state your intent of what the test will do — this must always be an absolute. You will typically see the word should in this part — this is standard, if not exactly imbuing the reader with confidence that your test will deliver…

Next, we have our assertion, assert Chapter1.hello() == :world . This is where we state our outcome. We check our actual value (the value returned by executing the function) against our expected value (the value we want to be returned) and declare that they should be the same.

So now we know what a test looks like, let’s solve our product request.

Let’s delete the prebuilt chapter_1_test.exs file and create two files:

lib/chapter_1/calculator_basic.ex

test/chapter_1/calculator_basic_test.exs

For ease, it is better to namespace files with a similar file structure, but this isn’t crucial. Personally, I find it easier to look for tests in a similar place to where they exist in the lib.

Referring back to our original product request:

We can put together our first test:

This looks similar to the prebuilt test but with two additions.

Line 3, alias Chapter1.CalculatorBasic , allows us to import the calculator we will build and refer to it in this module as the last value after a . — in this case, CalculatorBasic . This makes our code super clean to read and write. You can end up with multiple chains, for example Foo.Bar.Baz.Thing , and by aliasing it we can refer to it throughout the module as Thing ; Elixir will know exactly which Thing you are referring too ❤.

Line 5, describe "add/2" , is how we will provide context to our tests. By describing the function and the arity (number of arguments it takes), we can provide greater clarity to people looking at our tests, and also have clearer feedback as to which tests are failing.

Right, recap of our test. We will have a function called add/2 , it will take in the numbers 2, 3and return the sum 5 .

In true TDD fashion, let us run our tests and see what occurs:

mix test

It fails, which makes sense as we have not written anything just yet. So, if we move into the other file we created we can get this passing:

Ignoring the @moduledoc for now, we have a simple function add/2 which will return the sum of the values passed in. Let’s run a test and see how we do:

Ayyyy. And with that we open up a PR, merge it to master and deploy it to production. Success.

~ TWO HOURS LATER ~

An email from Product with an urgent request. Let’s see what it has in store:

… Ah. Right, oops. Oversight on our behalf, but let’s try and redeem ourselves quickly. Open up the test file and let’s add in another scenario:

Here we see the beauty of using describe to namespace our tests, as we can provide further context to our expected behaviour. So, let’s look at what Dave sees when he runs this in production:

Oh…

Ah, right.

So, what we would do in another language would be to accept the arguments like normal and then use an if else statement to handle anything that isn’t a number. While a solution, it provides further logic checks and more mental mapping, so let’s just use an Elixir approach:

Elixir allows you to redeclare the same function name but provide different arguments, and will pattern match and use the first one it finds.

So, what we are doing here is declaring that if add/2 receives two numbers, return the sum. If it receives anything else, return the message.

The _variablename notation is a way of indicating that we don’t care what the value is. In this instance, if a is not a number it doesn’t matter what type the value is because we will return the message.

The ordering is incredibly important, because the underscore is greedy. If we were to have it above our integer check, it would always match and so the message would always be returned.

Typically you will see the ordering as:

  • nil checks
  • pattern matches
  • greedy captures

Run mix test to see how we do:

Hurrah

Success. So we open a hurried PR, deploy to production and watch Dave’s grin vanish as he realises that we fixed his new toy.

A moment of reflection: we saw this bug coming.

By pausing to think, and sketching out some vague assertions, we could have had this work in one PR rather than two.

It’s easy to test the happy path, ship it and fix the bugs as they are raised. This isn’t a very holistic or a kind approach, as addressing P1 bugs is always horrific and your morale takes a kicking.

So what did we learn? Well, we should have been a bit more thorough. It was a combination of assumptions and laziness.

We must resolve to having at least a couple of scenarios for each function we write. Happy path is great, but also include a few sanity checks for those ‘what if’ moments.

It’s finding a balance, it happens all the time.

Let’s pause for now. Part 2 of this chapter will focus on other types of tests and basic assertions. Market research shows that these days calculators do a few more things…

--

--

hoodsuphopeshigh

Press buttons | Dance to spotify | Rule the universe | Eternal optimist