Scott Will and Ted Rivera Defensive coding techniques and good development test practices will help you produce higher-quality code in shorter periods of time and with fewer defects. ProblemA primary motivation for writing this book is set forth in the Preface:
Any software improvement or development effort needs to focus also on the outlook and habits of the individual developer, but the problem is that many improvement efforts tend to overlook this key item, partly because too many developers think that their job is merely to write the code and that it's then the tester's job to find defects. However, this mindset is the antithesis of agility, and it results in the need for additional effort later in the development cycle. By adopting simple defect prevention and early defect detection techniques, developers can produce immediate improvements in code quality and productivity, which in turn create momentum for wider adoption of these techniques as well as other improvement initiatives. This practice focuses on simple techniques that all developers can use to prevent defects from occurring and to help find defects before the code is given to a test team. BackgroundIt's a good thing that defensive driving techniques are hammered home in driver's education classes. You may have witnessed drivers running a red light while talking on a cell phone, swerving all over the road while drinking a cup of coffee, or being distracted by kids misbehaving in the back of the car. As part of defensive driving, we quickly learn to anticipate dangerous situations, as well as do our best to avoid them. As part of safe driving preventive measures, we make sure that we wear seatbelts and drive cars equipped with airbags and other safety devices. Defensive driving is a useful analogy for "defensive development." While most of us have been taught defensive driving techniques (and for good reason!), not all developers have been taught defensive development techniques. Sure, cars are equipped with seatbelts and airbags, but these are fallback measures in case the avoidance techniques don't work. Similarly, in software development we use testing teams, but they shouldn't be our first resort for preventing defects from reaching the end user. Rather, the testing team should be seen as the fallback measure if defects make it out of the development teams.
The remainder of this practice addresses both defensive coding techniques and good development test practices that will help you produce higher-quality code in less time and with fewer defects. Applying the PracticeFirst, a "no brainer" question: Which takes less time?
While the theoretical answer to the question is obviously number 3, in actual practice we too often choose answer 1 because of intense schedule pressures placed on us during the initial phases of product development. Such pressures must be resisted: defect prevention should be a top priority for developers and must be consciously and actively pursued along with a strong emphasis on early defect detection. Following is a discussion of several helpful defect prevention techniques to consider, broken down into three stages.
Prior to Writing CodeBefore you even start writing the code, you should consider the following items in detail: design, installation and usage, operating systems and databases, and testability. Think First: Design ConsiderationsSteve McConnell uses the example of moving enormous blocks for building the pyramids as an analogy for building complex softwarethere are many ways to move such blocks, and although it may take time to plan such movement up front, you will make far greater progress if you use adequate forethought.[1] Don't be content just to push hard! Ensure that the design is appropriate for the complexity of the method, function, or application to be developed. In some instances this will be no more difficult than forming a clear design in your head or on the whiteboard by identifying a set of test cases to be proved, or, when appropriate, through more formal means.
When looking at the use cases for the product, do any areas appear ambiguous? Is requirements wording vague, or even contradictory? Is anything unclear or confusing? Make sure to resolve these issues, especially if it appears that one developer could interpret an issue one way and a peer could interpret the same item in a different way, thus leading to confusion, incompatibilities, and ultimately delays and dissatisfied users. A great way to obtain an independent assessment of the requirements is to review them with an end user or an end user's representative. Without such input, you might not fully understand the requirements [2]
One additional point is worth emphasizing: the beginning of a project always has the highest degree of uncertainty. A common mistake is to spend a disproportionate amount of time up front getting the requirements "just right" instead of two days describing them, two days designing, four days implementing, and then a few more days updating the requirements, design, and implementation to make sure all elements are in synch. Writing code is a great way to validate requirements and designs, as long as you are consciously doing so with a plan in mind, but it should not be the only means by which the designs are created. Conversely, a tight schedule is no justification for just "getting started"; on the contrary, appropriate planning and design increases overall project speed.
Installation and Usage ConsiderationsFirst impressions matter. When users have to install, upgrade, or fine-tune software that is important to their organization, they are often "under the gun." If these activities are awkward or difficult, their perception of the software itself is often permanently skewed. Many defects can be avoided, and many users pleased, by considering installation, migration, and usage up front; however, these are too often tasks that are left to the end of a software project.
Has functionality been changed from an earlier release? If so, how will upgrading from an earlier release to this newer level be affected? Have parameters or other options been changed that may cause these upgrades to fail? Has an application programming interface (API) changed? Do other products integrate with this code? Will the latest changes impact that integration? Your software is often part of a mixed environment, and the ease with which it is deployed contributes significantly to the value users will derive from it and (for those customers who purchase your product) to the positive return on their investment. Understanding what changes have been made, and writing or updating code accordingly, can prevent these high-impact defects from occurring. As a side note, test teams should be thinking about installation issues early on as part of their test strategy. Scott Ambler has been hammering home for years the necessity of having testers perform comprehensive installation testing. See his discussion of this issue in The Object Primer: Agile Model Driven Development with UML 2.0.[3]
Operating System and Database ConsiderationsTeams sometimes assume that the code they are developing on the operating system or database in their office or cube will easily port to other similar operating systems or databases. The testing of other such important platforms is often either left to others or never adequately completed, thus frequently creating inconsistent levels of reliability across such platforms. Are there new releases of operating systems or databases that your code is expected to run on? Do you know what changes have been incorporated into these newer versions and whether they will affect your code?
One simple approach to consider wherever possible is having individual developers or independent testers routinely use different combinations of software as their primary development platform or test environment. This approach minimizes the cost and risk of platform testing but, surprisingly, is rarely considered. Regardless, if you are selling software that purports to run on key operating systems or databases, you must give adequate consideration to the necessity of ensuring success in these environments; you cannot treat this as an afterthought if the software is to achieve true success. Testability ConsiderationsThe increasing complexity of software, the rise of componentization and service-oriented architectures, and the need for software to be integrated into sophisticated environments makes figuring out how to debug errors up front a necessity, not a luxury. The harsh alternative is a lifetime maintaining code with weak testability. Do you plan to incorporate testability into your code? If so, do you understand what this entails?
What types of measures should you be considering in terms of testability? Here are a few to point you in the right direction:
No doubt you can come up with other items to add to this list, especially those that may be specific to your product or your organization.[4]
While Writing CodeThere are several techniques to consider using while actually writing code: defensive coding techniques, customer awareness, pair programming, and code inspections. Defensive Coding TechniquesLet's face it: defects will make their way into your software. Substantial problems arise from errors in design, architecture, requirements, documentation, complex or erroneous environment setup, and a myriad of other sources. This section focuses primarily on coding defects. A little forethought while writing code can go a long way to minimizing the number of defects injectedand that is what this particular practice is all about. While some defects are inevitable, many can be avoided by the application of defensive coding techniques that require little investment.
Customer AwarenessRemember, your customers think about your application differently than you do. They will install it in an environment that your team has likely never considered, or at least has not been able to test against. They will use it in ways you've never envisioned. They will configure it in ways you would never dream of. So your job is to try to make sure they don't get burned. Here are a few items to consider:
Pair ProgrammingTwo heads are better than oneand one implementation of this age-old axiom is pair programming. Pair programming is nothing more than having two programmers sit at the same computer and jointly write code: while one programmer is typing, the other is looking over his shoulder and providing both input and immediate feedback. Although pair programming has been around for years, it has only recently been "mainstreamed" by XP practitioners. You might think that having two programmers working on the same code would cut productivity in half. However, as many advocates of pair programming have discovered, the "lost" productivity is countered by increases in code quality. Think of pair programming as simultaneous code writing and code reviewing.
Code InspectionsWait a minute: this section of the book was supposed to address easy-to-implement techniques, wasn't it? And isn't the practice of code inspections an enormously time-consuming activity?
Here's the truth: there's nothing glamorous about doing code inspections, and they do take timeespecially the highly structured, formal code inspections originally articulated by Michael Fagan of IBM nearly thirty years ago.[9] However, they are also one of the best ways to discover defects. Aside from encouraging you, in the strongest terms possible, to perform code inspections, we'll leave the details concerning various aspects of inspections to those who have written extensively on them. For example, see Karl Wiegers's Peer Reviews in Software[10] for details on the types of existing code inspections (ranging from simply asking a buddy to look at your code to semiformal walk-throughs and desk checks, up to and including formal inspections); ways to implement inspections in your organization; and, especially, the tremendous benefits they provide.
To date, we are not aware of any head-to-head comparisons between teams using a highly structured, formal code inspection process versus teams using a less structured approach like pair programming. Studies have shown that the returns realized from pair programming are worth the investment,[11] and there is an extensive history showing the benefits of formal inspections,[12] so a word of warning is appropriate here: in the same way that an organization can become buried in documentation while detailing requirements or designs before ever writing code, so also reviews and inspections can degenerate into an end in themselves. So don't fall in love with the process, but with the goal: driving out defects. Let common sense prevail and choose the technique that best suits you, your teammates, your project, and your organizational culture.
When Testing CodeWhen the code is written and you are testing it, consider the following techniques: defensive testing techniques, scaffolding code, test-driven development, and source-level debuggers. Defensive Testing Techniques
In this book defensive testing is considered to be any testing and analysis that a developer performs on his or her code, after the code compiles correctly and before it is handed off to a test team. It is important for developers to pursue such testing actively and to have a tester's mindset when doing so.[13] Here is what "developer testing" should include:
This list is the result of a combination of years of experience in development and testing, extensive reading on the subject of testing (especially works by James Whittaker[16] and Boris Beizer[17]), and countless discussions with other developers and testers. It is by no means a comprehensive list; modify it as necessary according to your own imagination, creativity, and understanding of your own strengths and weaknesses.
Scaffolding CodeScaffolding code is the "throwaway" code you write to mimic or simulate other parts of the code that have not yet been completed (sometimes also referred to as "stubbing" your code). If you need to create it for your own use, don't throw it away; make sure you pass it on to the test team. It may be that the scaffolding code you provide will allow them to get an early jump on testing your code, or at least give them a better idea of what to expect when the other components are ready. It can also provide a solid basis for their test automation efforts.
If your product has protective security features, test those features carefully. Providing scaffolding code that can create the situation you are trying to prevent becomes very important: you must be able to create the very situations against which the system is attempting to protect itself. Another simple example of "scaffolding code" is providing code to manipulate queues. If your product makes use of queues, imagine how easy it would be to have a tool that would allow you to add and delete items on the fly from the queue, corrupt the data within the queue (to ensure proper error handling), and so on. Test-Driven Development
Arising from within the agile/XP development community is an important technique known as test-driven development (TDD). While still falling in the realm of defensive testing techniquesbut providing a solution from an almost opposite directionthe concept of TDD is that the developer first writes a test case and then writes code for that test case to run against. If the test case fails, the code is changed until the test case passes. And not until after the test case passes is new code written (other than the code necessary to make the next test case pass). The ideal of this methodology is that when a developer has finished writing his or her code, the code has already been tested, and a full suite of automated test cases exists that can be run by test teams, change teams, and even customers should the team so choose. Kent Beck, the "Father of Extreme Programming," has written about TDD in Test Driven Development: By Example,[18] which provides an excellent introduction to TDD. A newer work by Dave Astels covers TDD more thoroughly and has received much acclaim.[] Consider using TDD if you and your team have already implemented many of the techniques and practices discussed above and you are ready to take your improvement and development efforts to the next level.
Source-Level Debuggers
The use of source-level debuggers is one key way in which thorough individual testing can be performed. For developers, being able to use debuggers is vital; the benefits of source-level debuggers far outweigh any learning curve, and we certainly encourage readers to make the effort to learn a debugger, and to learn it thoroughly. Here are just a few ways you can use source level debuggers to test your code:
ConclusionAs strongly implied at the outset of this practice, preventing defects from occurring is a significant step toward improving code quality and developer efficiency. Further, finding any defects that do get introduced as early as possible also significantly improves product quality and efficiency. One of the best parts of this practice is that the techniques described work independently of any development methodology (e.g., iterative, waterfall, agile/XP) and generally cost virtually nothing to adopt. However, sometimes the use of such techniques seems counterintuitive: given the typical schedule and staff pressures cited in the introductory discussion of this practice, it is often tempting to sacrifice solid, strategic goals for tactical necessities. Projects and practitioners are often tempted to hit unrealistic or unreasonable schedules at the expense of using sound development techniques, but the implications are far-reaching and often affect many subsequent releases, not just the current one.
The ultimate benefits of considering these techniques thoroughly and implementing them intelligently and selectively in your organization will result in fewer defects in your code, fewer regressions, higher quality, and lower rework costs. But none of these benefits will occur unless the project leadership actively promotes an environment in which the adoption of these techniques will be welcomed, and even expected. The importance of establishing a culture that enables and encourages the adoption of techniques to prevent defects, or at least detect those that do make it into the code, as early as possible cannot be overstated. Without such a culture, much of what is discussed in this practice will likely fall on deaf ears. Other MethodsIt is difficult to say this without appearing cynical or sarcastic, but the following comments are the result of objective observation: employing none of the techniques described in this practicein other words, "doing nothing" (or almost nothing)is the primary alternative method employed when writing code. The time and resource pressures that we have alluded to are not fictional; almost every developer knows what it is like to be constantly under the gun. Generally speaking, developers are a conscientious lot, with an appreciation for concerns such as quality and craftsmanship. But when hounded for a particular deliverable on an unreasonable schedule, it appears necessary to be satisfied with producing code that "works for me" rather than writing code using many of the techniques outlined in this practice. To return to our opening analogy, this is the equivalent of "offensive driving," that is, driving along in the hope that all the lights will be green, the speed limits are mere guidelines, and other drivers stationary. In such circumstances, a collision is inevitable. Similarly, in the end doing nothing almost certainly means longer schedules and lower quality, resulting in ever-increasing schedule pressures; it's a vicious cycle. XP is very much a code-focused approach, and its techniques (including pair programming and test-driven development) are good approaches to improving code quality. Test-first design in particular ensures higher-quality code, because there is no opportunity for untested code. Pair programming, on the other hand, is somewhat more controversial. It adds a lot of value by ensuring that all code is looked at by two people, and it enables programmers to share their knowledge. Pair programming and test-first design are techniques that you should consider applying. The other techniques listed in this practice should also be considered. We believe that most development organizations will adopt a mix of techniques that work for them. Levels of AdoptionThis practice can be adopted at three different levels:
Related Practices
Additional InformationInformation in the Unified ProcessOpenUP/Basic covers basic informal reviews (optionally replaced by pair programming) and developer testing techniques. OpenUP/Basic assumes that programming guidelines exist and requires developers to follow those guidelines. OpenUP/Basic recommends that an architectural skeleton of the system be implemented early in development to address technical risks and identify defects. RUP adds guidance on static, structural, and runtime code analysis, as well as defensive coding and advanced testing techniques. RUP also provides guidance on how to create project-specific guidelines and apply formal inspections. Additional ReadingFor detailed books on software development, defensive coding and testing, and code inspections, we recommend the following:
For books that provide you with an understanding of how writing code fits with other lifecycle activities, such as design, implementation and testing, see the following:
|