Unit Testing Without Mocks
In my last post, I revealed my conclusions regarding what to do instead of misusing IoC containers and interfaces all over your code mostly for the purpose of unit testing.
One of the approaches I suggested was to focus on writing level 1 or level 2 unit tests and occasionally level 3 unit tests with 1 or possibly 2 dependencies at most.
I want to focus on how this is possible, using real code where I can.
First let’s talk about the approach
When I first talk about writing mainly level 1 or level 2 unit tests, most people assume that I mean to cherry pick the few classes in your code base that already qualify and only write unit tests for those classes.
That is not at all what I am advocating.
Instead, the approach I am suggesting is to find ways to make most of the actual logic in your code become encapsulated into classes that depend mostly on data.
What I mean by this is that our goal should be to refactor or write our code in such a way that logic is grouped into classes that only depend on primitive types and data classes, not other classes that contain logic.
This of course is not fully achievable, because something will have to tie all of these logic containing classes together. We we need these tie-together classes, but if we can make their job to simply execute commands and tie other classes together, we can feel pretty confident in not unit testing them, and we make our job a whole lot easier.
So to summarize, the basic strategy we are going to employ here is to eliminate the need for mocks by designing our classes to be much smaller, having much tighter roles and responsibilities, and operating on data that is passed in rather than manipulating methods on other classes.
There are many patterns we can use to achieve this goal. I’ll show you an example, then I’ll try to cover some of the major patterns I have discovered so far.
A real world example
I recently released a Java based Android application called PaceMaker. When I had started out building this application, I set out with the high and mighty goal of using Google Guice framework for dependency injection and BDD style unit tests using JMock to mock passed in dependencies. It wasn’t a horrible approach, I wrote about it here.
What I found with this approach though was that I was spending a large amount of time creating mocks, and I wasn’t getting much benefit from it. So, I had abandoned writing unit tests for the code all together, and I pulled out the now almost useless Guice.
The past couple of nights, I decided to use this project as a test project to demonstrate some of the ideas I have been talking about and thinking about.
I wanted to take my real code, refactor the logic out of it into small classes with little or no dependencies, and write BDD style unit tests that would be simple and easy to understand.
The big challenge here was trying to find a small enough piece of code to use an as example. For this example, I am going to use a bit of code that I was using to generate the name of a file that I write to disk for saving the data from a run.
This code was originally inside of a presenter class that handled generating a file name to pass to a serializer class in order to serialize the run.
Here is the original private method that existed in the presenter.
This method was a perfect candidate for some easily testable logic that could be put into its own class, but there are a few problems we should notice here.
- We are dependent on StorageManager to get the data storage directory used as the base directory.
- We are dependent on the locationTracker object to get the time of the first location.
- There is some real logic here in the form of a transformation. (It is important to note that we are dealing with logic, not just commands, because testing execution of commands is not as important as testing logic.)
My approach to this refactor is actually pretty simple. The first thing we need to do is see the dependencies for what they are. It looks like our logic is dependent on StorageManager and locationTracker, but in reality the logic is dependent on the string which is the base directory for the file and the time to use for the file name.
We can change this code to reflect that pretty easily.
What we have done here is small, but it is critical. We have eliminated dependencies that would otherwise have to be mocked to test this logic. Sure, we will still need to use those dependencies to get the data to pass into this method, but we can leave that code in the presenter class and move this code into its own class. The class will be small for now, but it will be easily testable with a level 1 unit test (our favorite kind.)
I didn’t cherry pick this example from the source code in PaceMaker, but I did cherry pick it for this blog post, because it was one of the shorter examples I could use.
I have several other areas of code in my presenter class in PaceMaker where I used a similar approach to extract out the logic, pull it into its own class with little or no dependencies and write unit tests for.
Here are two other examples:
- Pulled the state logic for starting, stopping, pausing, resuming and calculating length of time paused during a run into its own class. I elected to add one dependency (the presenter itself as an observer) to the refactored class in order to allow the class to notify the presenter when the state changed. In C# I would have just used an event to do this, but in Java we use the observer pattern.
- Pulled out the logic that created the GPX data files into a GPXDataModelBuilder which instead of depending on the LocationTracker class depended only on the data from that class.
The result ended up being very clear, easy to write, level 1 and level 2 unit tests with no mocking. In addition, my class structure is now much more tightly cohesive, with a much tighter and clearer responsibility. Before my presenter was doing several things, but now many of those things are broken up into very small testable classes.
In my next post, I’ll go into the patterns you can use to create classes that are able to be tested by level 1 and level 2 unit tests.