General musings on programming languages, and Java.

Saturday, September 26, 2009

Making Methods Testable

I'm going through my old drafts and deleting or publishing them. This one I no longer agree with, I'd rather rewrite the methods to be side-effect free. And the 'magic' I referred to is probably mocks.

"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things." -- Alan Kay.

Messaging seems to imply behaviour. From wikipedia: "in object-oriented programming languages such as Smalltalk or Java, a message is sent to an object, specifying a request for action."

It seems fairly trivial that, in, say, Java, Math.cos(double) is not object-oriented. It doesn't take any objects, it doesn't return any objects. Even if double was an object type, it wouldn't be object-oriented. One reason is that it isn't late-bound; the other is that it isn't a message that demands action, it's just a function call.

It's certainly possible to imagine an action that has no effects other than returning a value; I don't dispute that; but it does seem overblown to say "sending the message 'cos' to the Math object", instead of saying that Math has a function called cos, and we're calling that. In fact, function is a much better fit for this, because Math.cos is as close as we can get to a mathematical function. Whenever I say function from now on I'll mean a mathematical function.

Java actually has two implementations of all the maths functions, e.g., Math.cos and StrictMath.cos. We could make it so that which one we used was dynamically configurable (again, I'll cover late binding later), but that doesn't make it any more of an action. It's still a function. I assert that most of what we consider an object's behaviour can be validly and usefully thought of as functions over that object's data. In a simple way, you can see this is true; if an object has state X, and you call method x() on it, that changes the object to be in state Y, and you later return the object to state X and call the method again, it will do the same thing. The big difference between methods and functions seems to be that you can't see what side effects a method causes, without knowing the code.

Methods that have real effects on the world, or change objects, are really actions. Testing these is harder than testing functions. They're also harder to reason about and actually unnecessary.

I can think of two ways of testing them, the standard one of which is to simulate the environment and test for evidence of the action. This has the problem that any unwanted action isn't easily detected, except in the most carefully controlled environments. For example, you might set up a chroot environment for automatically testing a program that manipulates files, and not notice that it erases /etc/passwd (because you never use the chrooted filesystem again), or you might test what a method sends to System.out but neglect to also check System.err.

Another way of testing actions would be to first look at the code differently. Code doesn't do anything. It describes things. A program that writes data to a file doesn't really; it requests that data be written to a file. Intercept all interactions with the outside world. If someone wants to write to System.err, they do it through your testing code. The implementation is a little difficult to conceive; perhaps in Java it has to be done through aspects, or DI, if it is to be reasonably brief.

Once you have all the effects that an action produces, you can see if they match your expectations. There is a complication though; you could end up simulating too much of your environment. If a method asks how big a file is, you need to provide an answer. Ideally you would give a different value for each test, e.g., some exception saying the file is inaccessible, lengths 0, 1, and any other lengths that seem relevant to the method.

What we've actually done is to cocoon the method; we've now made it into a function! The perfect test suite for a method makes the method into a function. Well, that's all well and good for I/O, etc., but how about when we're testing a mutable object - it might have changed more than is usual to test for. E.g.:

public void broken()
    setX(5);  // we're happy with this line.
    setY(10); // but this one seems wrong.  We want the test to fail while this line is present.
.Let's magically jump in the way and grab a list of changes to the object that this method causes. We know x has been changed and y has been changed, so the test fails. In fact, for this case we never need to execute the actions, because we can see that they fail without executing them. For more interesting cases, we can again simulate the effect. Besides my using this technique to demonstrate a point, it could be used to test real running objects without changing them.

So again, the ideal test suite for a method makes it look like a function. If we adapt our code to explicitly state what effects it should cause, instead of going off and causing them, we then actually have functions in our code. Here's the above broken(), but fixed..

public List<Action> fixed()
    return asList(new AssignAction("x",5),new AssignAction("y",10));
The code is significantly uglier and doesn't use static typing, but it's now actually a function. Because I've adapted it to return stuff instead of do stuff, it is now far easier to test. Or perhaps all I've done is to make message passing even more explicit. Well, message passing involves actions, and to some extent we can invoke the above actions and observe their results without changing anything (recurse through the list constructing a chain of values, rather like SICP's discussion of chained environments).

It's now clearly far easier to reason about the method, because you can trivially observe all of its side-effects. You could even decide which ones to allow, externally to the code. In the usual case that you want to execute the effects immediately, you can still do that.

No comments:

Blog Archive

About Me

A salsa dancing, DJing programmer from Manchester, England.