Testing Elixir REPL Applications with Only ExUnit
Testing Elixir REPL applications with only ExUnit - Thiago Okada - Medium
Photo by Charles Deluvio 🇵🇭🇨🇦 on UnsplashWhile writing a REPL toy application in Elixir, I discovered that writing tests to the REPL can be surprisingly difficult. This is thanks to the nature of the REPL itself: reading user input (the Read part), evaluating the user input (the Eval part), printing to the user the result (the Print part) and finally starting all over again (the Loop part).Both Read and Print involves manipulation of Input/Output, that are operations that create side-effects. As Wikipedia defines it:In computer science, a function or expression is said to have a side effect if it modifies some state outside its local environment or has an observable interaction with the outside world besides returning a value.Side-effects makes testing our application harder since it generates an interaction with the world outside of the value that the functions returns. Let’s take the following example:This is a very basic REPL calculator written in Elixir. You can play with it like this:❯ iex -S mix           Erlang/OTP 20 [erts-9.3.3.1] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:10] [hipe] [kernel-poll:false]Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> Calculator.repl()accumulator> 0.0operation> + 1.5accumulator> 1.5operation> * 5                 accumulator> 7.5operation> = 0accumulator> 0.0So it seems to work, however we also want to test it to ensure that everything is alright. However, this application is also quite difficult to test, thanks to the side-effects.The repl/1function is our main culprit: it is difficult to test it since it depends on the input it gets from standard input in IO.gets/1 call.The traditional way to test it would be to actually run the application as a REPL and feeds its input using standard input redirection, using something like BATS. However, using some tricks it is possible to test this function using only what ExUnit offers to us.To start, let’s make some changes in the code above:So what did I did? Firstly I split the part of the repl function in a separate interpret/2 function, that allow us to run a single loop of our repl. This is important since we would need to write some exit condition to our loop otherwise or the test would loop infinitely otherwise.Afterwards, I substitute the call of IO module to a parameter to repl function (now repl/2) that I called io.At first sight, this seems silly: why the hell making IO a parameter of our function helps with testing? However after looking at the test file you can understand why I did it:So I created a FakeIO module that implement the two functions used inside repl/2: puts/1 and gets/1.We simple delegates puts/1 to the implementation in IO module itself, since we can capture the output from IO.puts/1 to the capture_io/1 from ExUnit.CaptureIO.The gets/1 is more interesting: we match the input of “operation> ” and returns “+ 1.0”, that is a valid input to our program. Afterwards we inject our FakeIO module in our interpret/2 function and asserts the desired output (in this case, “1.0”) in the end of our test.Now the explicitly io parameter in repl/2 and interpret/2 makes sense: it allows us to use alternative implementations of IO for specific purposes (in this case, testing). This is basically an usage of dependency injection technique, and in Functional Languages like Elixir it helps to maintain our functions pure (i.e. without side effects).The above approach is not without its problems. Let’s say for example we want to test a more complex case: a sum of 1.0 followed by a multiplication of 5 (for brevity sake, I will omit the module declaration from now). Sadly this doesn’t work:The problem is quite clear:warning: this clause cannot match because a previous clause at line 22 always matches  test/calculator_test.exs:23Of course, it will always pattern match the first definition of gets/1 and never the second one. So how can we solve this problem?I will basically “cheat” here, by adding a new feature to our REPL making it both easier to test and more useful to the user:I added a new parameter i to function interpret (now interpret/3), that counts the number of times the user entered a command in our REPL, and I increment this parameter at each iteration of repl/3 loop. This is similar to what IEx already does:❯ iex       Erlang/OTP 20 [erts-9.3.3.1] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:10] [hipe] [kernel-poll:false]Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> 1.0 + 23.0iex(2)> IO.puts("Hello World")Hello World:okThis helps the user to see how many commands it entered until them, and it also helps us to test our code:As you can see, thanks to the fact that now we have “operation(i)> ” , we can pattern match multiple gets/1 calls, one for each loop iteration that we want.So here we go, we can actually test REPL applications in Elixir using only ExUnit itself. As this article show, testing your REPL not only makes your application more robust, it also helps you to design better code that avoids side-effects and separate the concerns better.Special thanks to this StackOverflow answer for the inspiration to this blog post.