Thursday, November 05, 2009

Unit testing and programming paradigms

Today there is a large movement that exalts unit testing, most of the time when it is applied to object-oriented applications. But what about different paradigms? Is unit testing limited to classes and objects? Or you can unit test also C code?
There are different popular fields where you can try to put into practice unit testing: think for instance that even electronic circuits are unit tested. Let's explore the most famous programming paradigms in respect to testing practicality.

Unstructured programming (aka Goto and assembly jumps)
If we are writing low-level code, I think we cannot test very much extensively. Defining subroutines can help to define pieces of functionality, but fighting accidental complexity is nearly impossible. I remember rewriting my nokia snake game in assembly and how I had to be very smart in coding, since once the work was finished debugging and manual testing were the only way to discover errors. The application is not isolated from the machine as much as we would want (as we will see in the C case).

Structured/procedural programming (if, loops, functions and data structures)
In the classic C realm, testing can be performed on the defined functions, as we can get as close as we want to them from the upper layers. The problem manifests when we want to do the equivalent of injecting stubs and mocks in higher-level functions: there are no seams where we can substitute collaborator functions with stubbed ones, useful for testing. If my function calls printf(), I cannot stub that out specifying a different implementation (unless maybe I recompile everytime and play a lot with the preprocessor). Normally I would insulate printf() calls in an object I can inject, but there are no objects here, only static functions.
C is the high-level language which is closest to the raw metal and voltage values: performance and flexibility are great but there are no abstractions we can exploit. Function memory addresses are commonly hardcoded and relocated at loading time, unless you use function pointers. I guess a table of function pointers would perform the job, but in this case you can simply port to C++ where virtual methods are automatically implemented with this trick.

Object-oriented programming
In this paradigm we can inject collaborators in the class constructors or via setters, allowing tests to link fake collaborators and isolating the system under test on the upper ports (since the test call its methods directly) and on the lower ports (since it specifies stubs).
Note that if we are using static methods, the case becomes equivalent to structured programming as there are no seams to specify different methods that should be called. The upper layers test suite would perform integration testing and not unit testing as it would be forced to incorporate and exercise the lower layers to work.

Functional programming
In this paradigm, functions are first-class objects and they can be the argument of other functions. So instead of injecting collaborators in the constructor we could provide them as arguments, earning the ability to pass in fake functions in tests. The upper layers can thus be insulated without problems (with this sort of dependency injection) and there are no side effects that we have to take care of in the tear down phase - it seems that unit testing would be simple but I'm not an expert on functional programming, that's only what I would try.

In conclusion, I think unit testing trascends the object-oriented programming paradigm and it is a general practice. Let me know if you have other thoughts.

No comments: