Better IO testing through Monads

Unit Testing is a great way to try to verify consistent functionality over the life of our code. Typically we design our tests to cover ranges and edge cases of our code making sure that we can handle all inputs. This is made easy when we write in Functional Languages as there (should be) no side effects. But when you **want** to deal with IO and side affect code in Haskell, this becomes much more difficult.

This problem has been fixed many times, but often with very complex solutions. Let's see if we can make it really simple (even if the post is long).

If you want to play along the code is availabe from git or [zip file][7]

The Problem

While learning how to use [fltkhs][1], and creating a GUI [Diceware][2] password generator, I realized that I was going to be doing a lot of work in the `IO` Monad. For the GUI parts I didn't really care to do a lot of unit testing so I didn't think there would be much of an issue.

When we think of `IO` we often think of interacting with the User.

The `IO` Monad is used for other interactions such as random number generation. Here is a simple function that simulates rolling an "n-sided" die.

Using the repl we can see that our function returns numbers, but since its random we can't really see if the range of outputs are valid or not.

What makes this even worse is that because the value returned is random, doing any sort of edge case testing on code that uses this function is nearly impossible.

Mocks make simulation easier

Per [Wikipedia][2]

In object-oriented programming, mock objects are simulated objects that mimic
the behavior of real objects in controlled ways. A programmer typically
creates a mock object to test the behavior of some other object

Even though we are not working with a OO language, we can still take the concept of a Mock Object and apply it here. What better use for Monads than to create simulated scenarios to mock possible real world use cases.

So why Monads?

The feature of Monads we will be using deals with the fact that they allow us to temporarily modify the normal rules of haskell. This, of course, is done in a controlled way as to keep things still within the realm of typical Haskell and FP.

Being a pure functional language, Haskell functions are [referentially transparent][3]. This means that no matter the state of the application or the system it runs on, a function will always return the same result given an input. `sin()` will always return `0` when given the value `pi`. `toUpper` always returns `"HELLO"` when given `"hello"`.

Think about what rules change in haskell when you are in the `IO` Monad. When you call `getLine` the string returned is dependent on the user at the keyboard and two individuals may type two different values. This means that `getLine` is not referentially transparent. Within the context of the `IO` monad, its OK that this function doesn't follow Haskell's rules.

Haskell is also a (mostly) immutable language. New values are stored under new names, while old ones are cleaned up. Counters in loops don't exist, but instead are passed in as a counting argument into a recursive function. Updating values just isn't done.

But when you introduce the `State` Monad the rule of immutability is no more. Again, its a controlled manipulation of the rules, but you are able to change the application's state through a mutable object.

So what rules do we want to bend for our testing purposes?

The Testing Problem, Type Classes & Mocking

To repeat our problem, when running a unit test we can't rely on non-referentially transparent functions like `getLine` and `randomR` to give us consistent values. Because of this, it would be possible for a test to fail on one run, and be successful on another. This does not make for good testing.

What if we could write our code that uses the `IO` monad when we were running normally, but then run a special monad that mock's the `IO` monad when we do our testing? Then we could produce consistent results with our unit tests without having to redesign our code, or do modifications between runtime and test time.

Type Classes

This is where [Type Classes][4] come in. Type Classes are a way for Haskell to provide a polymorphic interface to types. You can define a set of functions and group them into a type class. Then you can declare instances of the type class with data types that exist, providing the implementation of its group of functions with relation to the instance that data type.

The `Eq` type class is used for testing equality. Members of the type class must provide `(==)` and `(/=)` implementations so that you can compare two value to be equal or not.

The `Ord` type class is used for ordering items, with the following definition:

Here we see that all members of the `Ord` type class must provide implementation to a hand full of different functions used for ordering. On the first line we also see `(Eq a)` which tells us that all types that are an instance of `Ord` must also be an instance of `Eq`. This makes sense since you'd be able to use equality during your ordering process.

Even monads are represented as type classes.

All Monads are Applicatives, with bind, return, and fail functions.

Making our own Type Class

Let's create our own type class. One that requires all the IO functions we will be using in our code that might affect our unit testing.

For our first operation we'll write a string to an output. In the `Prelude` library we have the following function:

A `String` is passed to the function, the value is written to stdout and then returns unit `()`...all occurring within the context of the `IO` monad. What we need to do is define our type class that works both within the `IO` monad as well as other monads. Since `Monad` is a type class, we can write our type class to only allow derived types to be part of the `Monad` class (just like `Ord` and `Eq`).

Our function `mPutStr` takes a `String` as an argument and then does "something" within the context of the `m` monad and returns unit `()`. In the `IO` monad we want that "something" to be writing to stdout. And since the signature of our function matches that of `putStr` we can easily define that for our `IO` instance of `MockIO`:

We can do the same for `putStrLn` and `getLine`. For our random number generator we'll define it to make our dice rolling simpler.

Here is the runtime implementation for `IO`.

So how do we use this new MockIO?

Let's go back to our dice rolling code and rewrite it using our new `MockIO` type class.

Pretty simple. We define our type `m` as an instance of `MockIO` and then use functions from within that type class. If we load this code up in the repl we get:

Since the repl is always running within the `IO` monad, we use the `IO` instance functions.

Mutable State as IO

So where does this get us? We now have the ability to call `IO` functions when we are working within `IO`. But in unit testing we want to remove the random results of `IO` with something fixed.

The `State` monad allows us to create a mutable state variable that exists during the context of the monad. We can use this monad to store expected values and return them as if they were pulled from IO. A simple explanation of `State` can be found at [LYaH][5].

The first step is to create the data type that will store our state. We want to provide fixed values for stdin and for random numbers, and it would be nice to store off things written to stdout.

Pretty simple, just a set of arrays contained inside a data type. We will derive `Show` and `Eq` type classes as to make debugging easier.

`State` has a type definition of

where `s` is the state information and `a` is the return type. The `runState` function is used to execute a function within the context of the `State` monad. Our use case with our new state object will be:

Rather than always typing `State MockState` we can define our own type:

For our unit test setup we will use a `MockState` to setup our environment and then execute the code using these new functions and data types. For example, we can use our dice rolling function and test that it returns the correct value.

We call `rollDice` within the `SimpleMockedIO` context giving the following function definition and we see the change:

Given a `MockState` this curried function would return an `Int` and the mutated state.

We defined the value returned from our "random number generator" and sure enough that is the value. Lets test it even more.

Six rolls and six exactly as expected results. So how is this done?

Pretend to be IO

Just like we did with the `IO` monad making it an instance of `MockIO`, we need to do the same with our new state monad `SimpleMockedIO`. For each of the functions we need to retrieve the current state, do whatever IO is necessary and then update the state for future use. Let's take a look at how the random numbers are handled.

We set the current state to `st` and then check to see if we have any numbers left in our mock environment. We check to make sure the next value is within our min and max (as we assume the way `IO` handles this would always work). Lastly we update the state by removing the number from the list, saving the new state off and returning our "random" number.

Writes to stdout are handled by concatenating to a list and reads are done in a similar way as the random numbers, just with `String` instead of `Int`.

Conclusion...our Unit Test Works!!!

While this isn't the most glamorous way of solving this problem, it does allow us to swap out `IO` for `State` which is what we needed to make our unit tests work. This solution can be extended to mock network traffic, interactions with a database or pretty much anything you want. It doesn't require a lot of knowledge in advanced topics, having almost everything in here being covered in [Learn You a Haskell][6].

Lastly, if you were worried that now all of your IO functions would be using this `MockIO` type class and not be portable, like if you were wanting to write a public library, don't worry. We can easily expose functions that have the correct `IO` monad type you want.

Since `IO` is an instance of `MockIO`, our definition of `rollDiceIO` just refines which `MockIO` instances will be using. Internally you'll use the `MockIO` version of code, along with your unit tests, but only publicly expose the `IO` only version.

fltkhs [1]

Mock Object [2]

Referential Transparency [3]

Learn You a Haskell: Types and Type Classes[4]

LYaH: For a few Monads more [5]

Learn You a Haskell [6]

Mockio v1 [7]

$ date: 2017-08-21 14:15 $

$ tags: haskell, monads, type classes, testing $

-- CC-BY-SA 4.0 jecxjo 2017-08-21

Comments?

back

Proxied content from gemini://gemini.sh0.xyz/log/2017-08-21-better-io-testing-through-monads.gmi

Gemini request details:

Original URL
gemini://gemini.sh0.xyz/log/2017-08-21-better-io-testing-through-monads.gmi
Status code
Success
Meta
text/gemini
Proxied by
kineto

Be advised that no attempt was made to verify the remote SSL certificate.