Tuesday, March 6, 2012

Static Methods Are Your Friend

There's an age-old debate about whether static methods are the devil. They make unit testing so hard!

Utility methods are easier to use when they're static. Take for instance a simple method to pull the value of a cookie off an HttpServletRequest. Since this is Java, we can't put the method on the HttpServletRequest class. We need to make a utility method instead, say in a class called CookieUtil.

public String getCookieValue(String cookieName, HttpServletRequest request);

Why inject or instantiate a CookieUtil just to call this method on it? If the method is static, we can call CookieUtil.getCookieValue(...) quickly and easily, without adding constructor arguments and fields that clutter up our classes.

The argument goes: you can't mock the method if it's static! (or, if you do, it's a big pain in the butt.) Our unit tests must test onnnlyyy this class!

First, let's consider only static utility methods which meet these two criteria:
* Referential transparency: the output depends entirely on the input. Same input -> same output, every time
* No side effects: there is no I/O in the method, and the objects passed in are not modified.

These static utility methods are highly testable. I think we should test the heck out of any static utility method, and then not worry about mocking it.

At some point we draw the line. Do we mock out the collections libraries? No! At some point you say, "This other code is tested separately. I am relying on it to work."

There's a compelling reason for this: your code is better tested when the utility methods are not mocked. When you mock a dependency such as CookieUtil, you're saying, "I'm testing that my class interacts with CookieUtil this certain way." What if that's not the way CookieUtil works? What if your mock of CookieUtil returns empty string when a cookie doesn't exist, but the real one returns null? Bam! NPE!

Environments (such as my previous place of employment) that emphasize purity of unit tests are vulnerable to these integration problems. Most bugs don't happen within a class - they happen at the seams where code written by various people intersects. Test this intersection!

Declining to mock utility methods is a step toward full testing. Test the low-level utility methods first. Then build on those, and test your classes including the integration with the real utility methods. This results in easier tests, less cluttered code, and better tested software. What's not to like?

1 comment:

  1. James made a great point: sometimes it's better to inject singleton utility objects in the constructor in order to have the option of mocking them.

    Don't mock them all the time; do test the interaction.

    For some tests, it's easier to mock the CookieUtil method than to mock getCookies on HttpServletRequest. It can make sense to have the option, at the cost of three lines of clutter in the class that gets CookieUtil injected. (constructor parameter, constructor assignment, field.)

    ReplyDelete