Testing is a poorly understood concept within the world of software development, especially unit and blackbox testing. I think the fundamental problem is in understanding the purpose of testing. When I first started as a developer, I thought of testing as something that someone else does to my code to find problems in it, and I fell into the trap of seeing myself as adversarial with testing. Now having built systems with large, extensive unit and functional tests written concurrently with the code, I have a totally different conception of testing. I treat my test cases like formal requirements. If all the tests pass, then I am meeting my requirements. If the system breaks and my tests didn't catch it, then my requirements were not completely specified and I need to modify my test cases.
This philosophy has some interesting consequences. First, developing, testing and debugging end up working in lockstep: every time a feature is added or bug is found, a test is added for it. One thing I really like about Ruby on Rails is that this idea is built in to the "generator" mechanism in that it creates test fixtures for every controller that you create. Second, I don't code "scared" anymore. I define coding scared as a refusal to make changes or add features because you are afraid of breaking your system in unexpected ways. I hated coding scared, but it's generally where you end up without tests. Since you have no way of quickly and comprehensively verifying the result when you make changes, you have rely on expensive and time-consuming manual usage to tell you what you've done wrong. Finally, this approach has made me much less resistant to change. In a traditional software development model, you spend a lot of time trying to determine how risky is and whether the risk is worth it. I worry about that less than I used to, because if something is going to screw my system up badly, I'll know it right away, and with good iterative development practices, I'll know exactly where things went wrong.
In my head, I imagine each piece of software as being built on a giant empty surface. As I make changes and add features to the system I am drawing and redrawing a shape on that surface. The problem is, the shape needs to avoid certain regions of the surface, regions which represent bugs or other problems. The tests that I write are little fences that keep me out of those regions, and as I write more tests, the shape that I can fill in becomes more and more clear. That is the true purpose of tests: they are the best way to constrain our systems and guide us to the ideal shape. With the fences in place you can worry less about making missteps and worry more about how to fill in the correct regions.
