RED ALERT! Brain dump at twelve o'clock!
It is time yet again to dribble out a bunch of half-finished thoughts about things I don't really properly understand. For this episode, my topic of choice is code integrity.
First of all, let me buy myself some time by defining the phrase "code integrity" as I intend to use it. It might be worth noting that I just completely made that up, so it's probably a stupid choice of words. That aside, however, I think the point I was originally trying to convey (before I got sidetracked in thinking up all of this self-referential gibberish) is that code can be broken. Deeply profound, I know.
So code breaks. The first important question is when did it break: was it already broken because the design itself was broken? was it broken because it wasn't implemented correctly? did it break during a maintenance change? did it break due to coupling to another piece of code (or data) which has changed? did the evil Nazi code gnomes sneak into the production system in the middle of the night and spread havoc?
Once we know when the code broke, we need to know something else: how the breakage was found. An excellent piece of advice (which, as usual, I stole from The Pragmatic Programmer) is to make sure that things fail as quickly as possible. A good example of this is division by zero. On a modern IA-32 processor, a division by zero actually throws an exception at the hardware level. This is a great idea. Suppose the processor didn't do anything, and simply expected the user to be smart enough never to divide by zero. Some random data could get stuck into memory, and cause all manner of bogus results.
Email is an example of how not to implement failure. In the common POP3/SMTP email structure, I send an email to firstname.lastname@example.org. Clearly, it never arrives. However, I don't know that this email failed until several hours later when the "I gave up trying to deliver your message" reply comes back from whatever server ended up barfing on my fake mail. If I have a spam filtration system, or if servers simply aren't configured to relay those messages, I may never know that my vitally important secrets about saving the world from Doomsday never reached my good friend Mr. Bogus.
In software, we've got some nifty things like assertions exceptions that let us make sure our code dies when things go wrong. Wait, wait, wait, though... aren't crashes bad? We don't want our users to see "ASSERTION FAILED IN FOO.CPP" when they run our program, right? Most likely, the answer is that no, we do not in fact want users to see that.
This brings up an important question. We want to fail as soon as possible, so that damage doesn't propagate into other areas of the code. We don't want to put an ugly "illegal operation" or "assertion failed" or "unhandled exception" type message on the screen. However, we also want to make sure that the information about the failure is preserved, so that the failure can be diagnosed and repaired.
There are some tricks we can use for that, like using structured exception handling hooks to capture OS and hardware exceptions (for C++ and Windows apps), logging systems, automated phone-home error reporting mechanisms, etc. However, those are beyond the scope of my rambling, by which I mean to say if I get started talking about that, I'll forget what it was I was pretending to talk about in the first place.
So, let's gloss over that issue for a bit. We now have a failure, and information about it. Assuming we have a good programmer handy, we also can fix the failure. We pat our trusty pager that got us out of bed at 3 AM for the fifth time this month, stumble out to the car, and drive home... oh wait, I have to be back at work in an hour... crap. One of those goofy proverb-things that your grandmother used to love to say comes back to mind... something about prevention and cure...
This is where it gets interesting. In fact, some very smart and capable people have already answered the question, a long time ago. You can tell that these people are smart, because they've answered the question, and I haven't even said what the question is yet. Deep Thought would be proud. The question is, can't we do something to keep these failures from happening at 3 AM and getting me out of bed?
The answer, of course, is
But unit tests are work, and I'm a lazy bastard. I hate work. Work is... well, work. It takes effort, and all of that stuff, and I just can't be bothered. Actually, I personally do run testing as much as possible, but only in a sort of lazy way. That's really why I'm dumping all of this; I think it should be possible to have unit testing that isn't work. The usual argument goes that the time spent writing and running a testing suite is easily won back in time not spent getting woken up by your pager at 3 AM. However, I think this is silly. I think I should be able to stay in bed at 3 AM, without taking the battery out of my pager, and without doing extra work making unit tests.
Here's the theory. First, we take our level-of-abstraction metacode thingy, and we generate code with it. Then, we use the same system to generate tests. This is based on a very arcane and deeply mysterious principle, which I shall now reveal to you, as Apoch's First Theorem of Testing Software:
Unit tests exist at a level of abstraction that is slightly higher than the module which the tests are designed to test. Therefore, both the unit test and the interface for the module itself can be specified completely at a level of abstraction that is slightly higher than that of the unit test itself.
The upshot of this is that it's getting late, my fingers are cold, and I want a sandwich. Sandwiches are in terribly short supply at the office. (Why I'm here doesn't really matter, although thankfully it does not involve a pager, and it doesn't include being awake at 3 AM. Yet. Sammidge.)
Oh, sorry, that's not the upshot of this. The real upshot of this is that unit tests can be generated with the same knowledge that generates the abstraction code. (This might not actually be an automated process. In fact, at the moment, it rarely is; the knowledge is stored in some people's brains, and the generation is done by typing and thinking.)
What's great about this is that, if we know enough to build a unit test, we also know enough to build a layer of abstraction. This means that the knowledge needed to generate both is highly coincident, if not identical. Let's call the set of knowledge needed to build the unit tests K(u), and the set of knowledge needed to build the abstraction layer K(a). Now, we'll introduce several contrived variables, and spend the rest of the discussion trying to make naughty words with our symbolic names.
I really should stop doing this late at night... I'm having a little trouble with that focus thing. Sammidge.
Now, for the sake of argument, let's say that the total knowledge about a module is K(u) U K(a) U K(o) where K(o) is other incidental knowledge about the module that isn't covered in the creation of the unit tests or the abstraction layer. I think that the K(o) set will always be empty, but I don't know for sure, so I'll leave it in there for now. Let's call all of this knowledge K(m).
Given some module m, and the specs for that module, K(m), we can therefore generate both an abstraction layer and a unit test for m. This should not be surprising. In fact, it's what software design is all about. However, the good stuff comes next.
It is quite likely, although I'm too lazy to logically prove, that there exists some common medium of representing K(m) that allows a nontrivial portion of the code for m and m's unit tests to be generated automatically. I hypothesize that the complete interface for m can be generated, and almost the complete unit test code for m. Actual implementation of m, and any dependencies of m that must play a role in the unit test, will probably have to be handled manually, but we're still ahead of the game.
We already need a medium to represent K(a) so we can build the abstraction layers automatically. Clearly, if we choose our medium wisely, we can use the same medium to generate quite a bit of code, all from a high level of abstraction. Our whole goal here is to work as abstractly as possible; by doing some mental trickery, we can actually work at a level of abstraction that is more abstract than the level of abstraction that we are trying to create.
I think there's probably some more that could be said, but that last bit really cooked my brain. Speaking of cooked brain, I'm really hungry. Grarghhh.