How can one possibly test before writing the code first? But then, how do we write code before our colleague has written the module we need? We are always coding to interfaces even if unconsciously. We can do the same for tests. In fact, we should not write any code till we know how to test it.
A widely quoted, memorable line of Prof. Dijkstra is that testing can show the presence of errors, never their absence. Why do we then emphasise testing so much. Our motivation is to suggest practices which will help reduce the effort we spend on testing. Only then can we focus our attention on programming activities far more important than testing. Testing is a necessary but hardly sufficient evil.
I recall typing an 'l' (the letter) instead of '1' (the number) on a punched card. It took me a ridiculous amount of time to discover the error. One lesson I learnt was that never use the letter 'l' as the name of a variable. Another time we cleaned up the code after thoroughly testing it. We removed the debugging statements and accidentally deleted an additional statement. The result was a devastating problem whose symptoms were visible in a module far removed from the affected module. In neither case was it a logic error. It was a consequence of carelessness, a trait present in most humans to varying degrees. Testing is necessary to save us from ourselves.
JUnit is a brilliant framework created by Kent Beck and Erich Gamma based on Kent Beck's testing framework for Smalltalk. A quotation Martin Fowler on the junit.org site is worth reproducing:
"Never in the field of software development was so much owed by so many to so few lines of code".
XUnit has been adapted for various languages. We will use PyUnit framework of Python to illustrate our ideas. This is by no means a tutorial though we hope the simplicity of the whole mechanism will encourage readers to start using these frameworks and learn the functionality as they need it.
Consider a little method we need to convert a string into a number. We expect to pass a string as a parameter and get an positive integer back as a result. We can satisfy ourselves that the code is functioning as intended if “0”, “1”, “99” return the correct numbers. We also know the method should fail if we try to convert '-1” to a number – that is our method should return an exception.
Note that before we have written any code, we know what to expect. This is, indeed, has to be, true for every class or method we write before we write it. We can't write code following 'stream of consciousness' as we would a novel. In that sense, it is not art.
Let us now look at a little code.
We import the unittest module and the functions we need to test. It is pretty obvious that the class TestCase has to be overridden because that is the code which has to be specific to the code we are testing. We use the convention that each test case method is prefixed with 'test'. This allows the framework to introspect and find the tests which need to be run. Typically, we expect the answer returned by a method to be a specific value or the method should return an exception. The frameworks allow many more options which can also be useful. and easily learnt when needed.
Finally, we run the test cases after our own code is ready. Stressing the obvious, how much is the effort for these test cases many-many times? Suppose someone asks, what should be the answer if our string contains ninety-nine 9's. Adding that test case as well would be a trivial effort. (For non-Python folks, the method will work and return a large number, very useful for cryptography.)
A side effect is that the way we think about programs can undergo a change. Consider a user interface program. We often have a situation where if some value is present in one field, another field should no longer be enterable. Underneath the UI, there is a data structure which tells the program what to do. In all probablity, there will be a boolean attribute of a field which tells whether data can be entered in it by the user or not. Similarly, the data entered in a field alters the state of some data structure. We can, thus, split the testing of UI into tests of data structures and look and feel of the application. We can write test cases to ensure that the data structures behave in the way that is intended. A very important corollary is that when we check the look and feel of the application, we do not have to worry about the correctness of the data. That is a separate issue to be handled separately. Even those could be unit tested, e.g. the size of a button, the background colour, the alignment, etc.
Test frameworks are not much harder in other languages though the language syntax does make it seem more complex. There is no harm in using Jython for testing using JUnit even if the application is in Java. Anything which makes testing easier should be welcome.
We hope that these practices will leave the programmers with enough time to reflect on their code and be proud of it even a year later. These practices should also then give the confidence to create and participate in Open Source projects with a greater comfort level that the the code we write will stand up to a review by the finest programmers in the world and our unit tests will ensure that the modifications made by others will not break our code by accident. Why does that matter so much? Well, I wish I could recall where I read it: If mankind destroys the world, it will have been by accident.
Other Articles >