4

I'm currently reading "Composing Software" by Eric Elliott, which is about functional programming in JavaScript. He states that if you compose multiple functions together, and that these functions have been fully tested in isolation - then you don't need to unit test the composed function (as that can be done using an integrated test that performs all the side effects.)

But isn't simply testing the individual functions violating the core TDD principle of "always test the public inteface, and not the implementation"? Arguably the composing of the smaller functions is an implementation detail. As long as my service performs the action that it says it will do, we shouldn't really care about the small "private" functions being composed in order to get the right behaviour.

I'm trying to reconcile the two ideas but I'm struggling. The only way around this (that I can think of) is to mock the dependencies that my service needs so that it can be tested through the public interface, but going functional was my attempt to stop using mocks in the first place.

3
  • 1
    Ask 10 developers and they will tell you how their testing approach is the correct one and other 9 are wrong. There is rarely consensus on what and how to test things. – Euphoric Oct 17 '20 at 10:32
  • 3
    Actually, it's more like "Ask 10 developers and you will get 11 answers with 12 arguments against 13 other approaches." – Jörg W Mittag Oct 17 '20 at 10:49
  • the unit test of the composed function tests whether you have composed it correctly, not whether the composition functions as composed – Ewan Oct 17 '20 at 10:59
6

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.

5
  • Thanks for the long reply, I agree you've said here. What about the scenario in which I have a service that has a method, say service.saveFile(...), which performs a bunch of side-effects (such as checking whether the user as permission, and then saving the file)? I want to be able to test all the behavioural cases through this public interface, which means that I have to mock something or call through to the real file store. Basically I can't verify that this method does what I want it to do by simply testing the individual functions. – user1474326 Oct 17 '20 at 14:47
  • I wont deny that mocking can become an issue but this example confuses me. If the file system knows the users permission let the file system complain when its insufficient. If the app knows then why ask the file system? I don't see the reason to mock permission checking here unless the design is poor. – candied_orange Oct 18 '20 at 10:43
  • In the application I am building, I am trying to allow users to upload files to a data lake based on whether they've been granted permissions by an admin. The permissions are based on the user's role and the organisation that they belong to. – user1474326 Oct 19 '20 at 18:45
  • @user1474326 If that’s the case then permission denied errors should be popping up before the data lake is touched anyway. – candied_orange Oct 19 '20 at 18:50
  • Yes, that's exactly what I'm doing in this service method. Checking for permissions then rejecting before any file operations would otherwise occur. – user1474326 Oct 20 '20 at 11:47
1

Maybe it will help if I put this in OOP terms first. A class has public and private methods, and you unit test via the public methods. But sometimes you make calls from that class into other classes, and those classes also have their public methods unit tested, even though those public methods are technically implementation details of the first class.

Functions aren't any different when they're not grouped into classes. Some are private and only tested via an enclosing function, and some are implementation details to one layer but the public interface to another.

You do need some tests to make sure you did the composition correctly, but if you try to test every combination of every composed function, you end up needing an exponential number of tests. You simply can't test thoroughly without testing in layers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.