Tips to increase testability
Testability depends on factors such as controllability, observability, availability (of executables, information), simplicity, stability, separation of components, and availability of oracles.
OPT FOR SIMPLICITY
Simpler designs are often easier to test. That is one more reason to choose simplicity over complexity. Besides, we are more likely to mess up when we try to be clever.
USE ASSERTIONS, USE EXCEPTIONS
Use exceptions where appropriate. Use assertions liberally. They are not the same (find out the difference), and the two are not interchangeable.
It has been claimed that MSOffice has about 250,000 LOC of assertions (about 1%). Various past studies have shown up to 6% of code as assertions in various software. Microsoft found that code with assertions has a lesser defect density.
PROVIDE A TESTING API
Testability does not come for free. You may have to add methods to increase testability. Some examples:
Void methods are hard to test. You can add other methods to check the effect of void methods.
Sometimes it is troublesome to verify whether a retuned object is same as the one we expected. We can override the toString method so that objects can be converted to strings which can then be compared easily. Another option is to override the compare method or overload the "==" operator.
A module can collaborate with other modules during execution. During testing, we might want to replace those collaborating objects with stubs or mock objects. This is called dependency injection. Consider providing an API for such dependency injection for modules you develop.
To test certain functionalities, you first have to bring the system to a certain state. Providing a method to easily bring the system to a given state helps in such cases.
USE BUILT-IN SELF-TESTS
For correctness-critical parts, you can develop the same function using multiple algorithms and include them all in the system. During runtime, the system will use an internal voting mechanism to make sure all algorithms gives the same result. If the results differ, it will fire an assertion.
DECOUPLE THE UI
User interface testing is harder to automate. Decoupling the UI from the system allows us to test the rest of the system without UI getting in the way. Here are some hints:
Avoid interacting with the user from a non-UI layer. For example, the business logic layer should not print a message for the user. Instead, the message should be passed to the UI layer to print.
When passing data from the UI to a lower layer, try to pass primitive or simple objects rather than pass them as complex domain objects.
If input values are supposed to be validated by the UI, put assertions in the receiving layer to ensure they really are.
If your system periodically writes to a log file, this file can be a valuable resource for testing and debugging. Sometimes, you can simply use the log file to verify the system behavior during a test case.
Tips for developer testing
Make sure you do a plenty of developer testing (unit and integration testing). Note that debugging is not developer testing.
FOLLOW THE IMPLEMENTATION STRATEGY
In the design by contract type coding, users of your module are responsible for the validity of the input values passed to your module. Your module does not guarantee anything if the input values are wrong. That means you do not have to test the module for invalid input. If the language does not have inbuilt support for DbC, you can use assertions to enforce validity of input values.
In defensive coding, you do not assume that others will use your code the way it is supposed to be used; you actively prevent others from misusing it. Testing should follow the same philosophy. Test the module to see whether it behaves as expected for invalid inputs.
Writing random test cases that just "feels right" is no good.
ACCEPT YOUR FALLIBILITY
Do not expect a piece of code to be perfect just because you wrote it yourself (there is no such thing as perfect code).
"TRIVIAL" CODE IS NOT IMMUNE FROM BUGS
Very small and simple modules can contain errors. They are so easy to overlook precisely because you do not expect them to have bugs.
DO NOT TEST GENTLY
Being the author of the code, you tend to treat it gingerly and test it only using test cases that you (subconsciously) know to work. Instead, good testing require you to try to break the code by doing all sorts of nasty things to it, not try to prove that it works. The latter is impossible in any case.
MAKE TEST CODE SELF-DOCUMENTING
It is unlikely that you will write a document that systematically describe every test case you have. But you could still make your test code self-documenting. Add on comments (or some other form of external documentation) for information that is not already apparent from the code.