Testing Without Objects — I Like It
Unit testing behaviours that depend only on functions has been a revelation for me. Without the labour of having to mock objects, I find creating tests easier and feel more satisfied with the resulting code which is cleaner, clearer and at-least as expressive.
I’m not here to bash OO or to promote the one true way. With demonstrative code examples I am instead going to continue where I left off in my last post explaining the benefits I am seeing from composing applications mostly of functions and with fewer objects.
Last time I demonstrated the benefits of using just functions and data to create application services. This post shows their benefits on unit testing.
No More Mock Objects
Key to all of the benefits of unit testing solely with functions and data is the fact that you no longer need to mock objects. I’ve known for a long time that most of the discomfort and displeasure I’ve had from unit testing is related to object mocks.
Mocks take time and effort to set up. Sometimes just the thought of having to faff around with mocks makes me not want to write tests. Mocks are also noisy. I’ll often look at tests and resent mocks for the noise they add.
When composing behaviours purely of functions, though, object mocks aren’t needed. The resulting unit tests are a joy to the eyes of any mortal soul. To demonstrate there are a few code samples below, starting first with the mock version:
This code follows the sample from the previous post, representing the recommend a friend use case of an online casino. You can see the full implementation and collaborator interfaces on github.
Now for the version composed purely of functions and data.
Immediately the difference in size alone is striking. Without mocks there is approximately 35% less code. LOC is not the only critieria though. To my eyes, the functional version is as at-least as expressive and was certainly less hassle to create.
The mechanics of this functional version are explained below. Even though it’s Scala, I promise there is no witchcraft, wizardy or type-system voodoo. It’s just a few boring functions.
Mocking functions is unarguably easier than mocking objects. You don’t need to create specific mocks for every object. You just need a few functions that return stubbed values.
In my code example I used the ret() function. Here’s what it looks like:
It’s the equivalent of Mockito’s when(x).thenReturn(y) but without all of the syntactic verbosity. I have a library of these so that there is one for every function arity.
You can also create specialised mock functions. In Scala, we have a few ubiquitous monads — Option and Future. If you look back to the code sample you’ll see retF(). That means return the value wrapped in a future, and is one of the reasons why the code is more concise than the mock version. The implementation is just another boring function:
You can also create similar utilities for verifying side effects. I use the “set()” function for this as shown below in a small fully self-encapsulated example.
There are a few other benefits of unit testing with functions and not objects that aren’t game-changers but are worth noting.
Stronger Commitment to Testing
it’s easier to stay committed to writing tests and not follow urges to skip the odd test here and there. Those emotional impulses often turn into regrets. So anything that helps me resist them is a definite win.
Streamlined TDD Workflow
I’ve been a TDD enthusiast for about five years, and that’s not going to change now. On the contrary, when I don’t have to mock objects it makes the iterations of TDD smoother and faster.
Reducing a few keystrokes isn’t a big win. But when you can stay focused on the problem and not have to faff around with interface signatures and mocking objects, it does help you to enhance flow and maintain motivation a little.
Reduced Refactoring/Restructuring Friction
You want to make a small change to your code, but you have to move behaviours from one object onto another. Most developers I have paired with resent this kind of friction caused by tests that rely on mocks. But when you’re not mocking objects, there’s a reduced amount of friction to deal with.