functional programming – Doesn’t “Always test through the public interface” contradict testing of individual composed functions?

It’s not the size of the function. It’s how it’s used.

Let’s take some well tested functions, + - * and Math.sqrt(), and compose them into a distance function:

function getDistance(xA, yA, xB, yB) { 
    var xDiff = xA - xB; 
    var yDiff = yA - yB;

    return Math.sqrt(xDiff * xDiff + yDiff * yDiff);

kirupa – using the pythagorean theorem to measure distance

All these little functions have been tested. This code follows the well proven pythagorean theorem. So we’re good right?

Well, no. Because we happen to know that the inputs 59.3293371,13.4877472 to 59.3225525,13.4619422 are supposed to give us 1.6.

The problem wasn’t with the little functions, or how they were composed. It was how they got used. The pythagorean theorem works with cartesian coordinates in a two dimensional plane. Not with latitude and longitude on the curved surface of the earth. We can sometimes catch errors like this by testing against expected results. But those expected results can’t always be pushed down into the smaller functions.

Some might think of this as integration testing. I still think of it as unit testing. Because a unit isn’t a class, or a function. A unit is a testable, deterministic, side effect free, chunk of code. Syntax doesn’t decide what it’s boundaries are. Structure doesn’t decide what it’s boundaries are. Behavior does.

Here are some unit testing rules that might help make this clear.

A test is not a unit test if:

  • It talks to the database
  • It communicates across the network
  • It touches the file system
  • It can’t run at the same time as any of your other unit tests
  • You have to do special things to your environment (such as editing config files) to run it.

Michael Feathers – A Set of Unit Testing Rules

Notice nothing was said about functions, classes, packages, objects, or procedures. Your code structure is not the issue here. It’s about behavior.

So I think of a unit as any chunk of code that you can carve out to test, so long as it follows these rules.

Does this mean every function must have tests written against it? No. Every function should be tested against how it’s used. Private functions have a limited use so they can be tested by testing the public functions that use them. Make that use wide spread though and you’re going to need more testing.

Focus on use and behavior.