What I Learned Writing My Own Mocks
During the past two months, I’ve been building a simple HTTP server built on Java. Throughout that time, I’ve learned a lot about HTTP specifications, agile development, object-oriented programming, refactoring, and test-driven development. One of restrictions that has simultaneously frustrated me and taught me the most has been the prohibition from using external libraries, including mock libraries.
Because this project is entirely driven by tests that I write, I realized pretty early on that it would be necessary to integrate mocking into my unit tests. Otherwise, my tests would become fragile. A simple GET request to a resource in my server currently involves ten different classes reading, parsing, building, handling, and writing, often passing objects along this path. If the test for one class depended on another class working correctly, I’d essentially be daisy chaining a ten-part test. This fragility would obscure the source of bugs.
Because of the restriction on using external libraries, I couldn’t use a mocking library like Mockito (but that’s OK because Mockito is a pretty terrible pun). Instead, I built a few of my own mocks. Here’s a bit of what I learned.
I grasped the benefits of dependency injection
When building something as complex as an HTTP server (even a simple one that covers the basics), classes will inevitably interact with one other. They’ll be passing in objects as parameters, and they’ll probably call those objects’ methods. When unit testing — that is, testing the functionality of a single class — we don’t want to rely on potentially faulty external classes.
Even if that class whose methods we’re calling is fully tested, it’s still not a great idea to rely on the actual implementation of those classes. To give just one trivial example, suppose I want to get some work done during my commute to work or home. If I have a class that connects to an external database, I’d need to be connected to the internet. Any tests that rely on a database would fail even if the code was written flawlessly.
One solution would be to avoid passing in objects. Of course, that solution completely defeats the purpose of object-oriented programming. At that point, you might as well be building with a functional programming language. Since functions in functional programming avoid changing state or causing side effects, you wouldn’t really need to mock behavior.
Although I would be far too intimated to build a server in a functional programming language, one important concept that I did glean from functional programming in Clojure is that state must be passed into functions since state is immutable. In a similar way, we can pass in the “state” of an object when a class depends on an external class. Injecting the dependency — as opposed to instantiating it within the class — makes it trivially simple to inject mocks and eliminate the need to depend on external class.
For example, a Router
class that routes both HTTP requests to different Handler
classes and also responses back to the class writing the HTTP response might logically have a default router when no route is found. If I decided to hard-code the default Handler
within the Router
class (perhaps as a final static variable), I wouldn’t be able to test that requests get routed to that Handler
without running assertions against the response that comes back from that Handler
. Any time I changed the functionality of that Handler
(or even the default handler entirely, as I’ve done a few times now), I’d have to change the assertion in RouterTest
. Instead, I can construct and pass in aMockHandler
class that returns a custom response without worrying about what the actual response from the default Handler
might be.
Mocks can speed up development
Before implementing mocks in some of my test classes, I kept finding myself working on two or more classes simultaneously just to keep my compiler happy. If I made a change in a class A, and if I passed in an instance of class A to class B, the tests for class B would often not compile. I’d have to jump between both classes until I resolved all errors. This back and forth made it difficult to take a break from my work. If I did, I’d often forget where I left off in my attempt to keep the test classes from breaking.
With mocks, I focus exclusively on building one class at a time. In the event that I haven’t implemented a method on an object passed into the class that I’m testing, I simply add a mock function to the mock class and pretend like its working as it should.
For example, the functionality to handle PUT
requests to a resource that already exists in the server involves two classes: FileHandler
and Directory
. The first reads the HTTP method from the incoming request to determine what to do with the request. For aPUT
request, it calls Directory
‘s #overwriteFileWithContent
method (which returns a boolean if it succeeds). I chose to follow a top-down approach to building this feature, so I started with testing FileHandler
. When my compiler complained that the #overwriteFileWithContent
method did not exist, I simply added the method to MockDirectory
and had it return true. The use of the mock there allowed me to build and commit a fully functional handler without distracting myself with building and testing a separate method.
Unit tests are not acceptance tests
With coding, I find that I often learn what something is by learning what it isn’t. Unit testing tests a class in isolation. For classes that use objects from another class, you can simulate isolation with mocks. It’s not fool-proof, but it gives you far more control over what the methods in external classes return. In short, they eliminate surprises. With acceptance testing, you wouldn’t be using mocks because you need to ensure that every class works together as expected.