Java Static Methods, Clean or untestable?

Recently I’ve come across several folks advocating avoiding using static methods in Java. Their chief objection seems to be issues around testing them. I don’t buy into that objection, I feel the benefits outweigh the issues.

What Benefits?

Static methods are analogous to functions, and should be thought of that way. If you’re considering a static method for the sake of visibility, i.e. a Singleton replacement, you’re barking up the wrong tree. The value of a static method is that it should have no side effects, and should operates only on the arguments passed in, and return a result. Being free of side effects should in fact make testing the method very easy. A test can set up the arguments, call the static, and then review the arguments and return value. Done. There’s no instance state to set up, and no unforeseen changes to the instance state. If you find your static method stating to maintain state outside itself in static instance variables – that’s a bad code smell – consider some refactoring.

So What’s the Problem With Testing?

The trouble with testing is when you’re testing a larger scope that employs a static method and you want to change that methods behavior. Consider the following, it’s a contrived example, but will serve:

public float chargeByMillis(int count, int rate) {
    long start = System.currentTimeMillis();
    for (int x = 0; x < count; x++) {
        do_something();
    }
    long end = System.currentTimeMillis();
    return rate * (end - start);
}

How do you test that the charge is calculated correctly if you can’t control the number of milliseconds? At least on of the mocking frameworks does allow you to mock static methods, but I wouldn’t go that way. First provide an interface for the code you want to control:

public interface TimeProvider {
    long currentTimeMillis();
}

Then provide a change to the method signature:

    public float chargeByMillis(int count, int rate, TimeProvider tp) {
        long start = tp.currentTimeMillis();
        for (int x = 0; x < count; x++) {
            do_something();
        }
        long end = tp.currentTimeMillis();
        return rate * (end - start);
    }
    
    public float chargeByMillis(int count, int rate) {
        return chargeByMillis(count, rate, new TimeProvider() {
             public long currentTimeMillis() {
                 return System.currentTimeMillis(); 
             });
    }

Now you can inject any TimeProvider for testing you want. You end up with clean code, that is easy to test.

The Point?

Don’t bail out on good coding practices you believe in to accommodate your testing frameworks. If you weaken your code to make it more testable, you’ll need more tests, and those tests will likely end up tightly coupled to both your code and tests implementations, and that tight coupling is just tech debt waiting to be addressed.

Advertisements