Better functions for improved testability

Benjamin Petit
Apr 17, 2020 · 4 min read

Pure functions, as defined by Functional Programming, can provide a simple set of rules for one to follow to improve code testability.

🤔 What are pure functions?

Pure functions, when called with a given input, compute and return output and do nothing else. It’s a mapping of values to values.
Pure functions should verify these three rules:

  • total: there’s a result for any possible input
  • deterministic: the result will always be the same if the input is the same
  • no side-effect: the function does nothing but computing the result

Pure functions are crucial in Functional Programming for “referential transparency”. It allows for composition of functions in a way we can reason about, much like mathematics. But even if you’re not interested in adopting Functional Programming, you can use this definition to improve your code.

😰 Why impure functions may make your code harder to test

Impure functions (functions that break one or more of the rules listed above) are harder to work with:

  • A partial function is one that has preconditions. The compiler can’t help you to make sure you’re always verifying the preconditions. You need to read the code or the documentation to know about them and write code and tests for that. Calling the function with unsupported input will cause issues (e.g. dividing by zero)
  • A non-deterministic function may return different results when called with a given input. Such functions can hardly be unit tested, as you can’t call the function with fixed input and compare the result with an expected value.
  • Functions with side-effects, much like partial functions, require knowledge of their internals. It does more than it says on the box if the box is the function signature. So like partial functions, compilers can’t help you. This time though, the problem is not that some input is invalid, it’s that somehow, part of the result is “hidden”.

🐛 Impurity spreads

One important thing to note is that impurity tends to spread. Let’s take an example of a function that can tell me how many cookies I can have today. When it’s my birthday, I’m allowed to have two cookies, instead of one:

Put that cookie down! Now!

In “isMyBirthday()”, we use “Date()” which is a non-deterministic function (it creates a Date object with the current date and time which won’t return the same result if we call the function today and tomorrow). “isMyBirthday()” becomes non-deterministic, as a result will change over time.

Finally, because “isMyBirthday()” is non-deterministic, “maximumCookie()” will be too (the function returns a different value if today is my birthday) and any function that uses it, and so on.
The same thing would happen with partial functions or functions with side-effects.

We can see that using an impure function is not a local problem for testability, and can have a significant impact on our test coverage.

🧹 Refactoring impure functions

Can we make all our functions pure then? No. You will still need some impure functions to write any valuable application — like calling an endpoint, storing a value in the UserDefaults, getting the current locale to format numbers, etc. But we can try to remove impure functions from the parts of our app that matter most like our business logic, that we would like to cover with tests.

Whenever you find a function that is hard to test, identify which rule is not satisfied, and follow the offenders until you reach a piece of code that causes the issues, and you can’t change. For our example, it would be when we call “Date()” in “isMyBirthday()”. Start by fixing the caller of this function:

  • Is the function partial? You can change the input type to enforce the preconditions, or you can change the return type to account for unsupported inputs (“Optional” works well, or “Result” if you want to provide details about the broken precondition)
  • Is the function non-deterministic? You may want to find a way to inject the state you depend on (could be a value or a function)
  • Does the function have a side effect? Change your return type to one that describes the side-effect, without performing it, or pass the impure function as a parameter. Run the effects, or define and pass the impure function, at the outer layer of your application, for example, in your UIViewController.

Your function is now pure, and you’ll need to refactor the code using it. Repeat until you can test your initial problematic function.

Let’s refactor our example:

This is how you use the function, for example in your UIViewController:

You can now test your cookie distribution logic:

🍪 Parting words
Understanding what makes a function impure is an easy way to know why a piece of code is hard to test. Refactoring still requires a bit of practice, but it makes it easier.
Pure functions can also help much more than this. Join the Tide iOS team if you’re interested to see how we use these concepts in our app:

Behind the scenes of the Tide Engineering team in London…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store