Skip to main content

Diagnosing Java Code: Designing "testable" applications

These seven principles build a base for code design with testing in mind

Eric Allen (eallen@cs.rice.edu), PhD Candidate, Rice University
Eric Allen has a bachelor's degree in computer science and mathematics from Cornell University and is a PhD candidate in the Java programming languages team at Rice University. Before returning to Rice to finish his degree, Eric was the lead Java software developer at Cycorp, Inc. He has also moderated the Java Beginner discussion forum at Javaworld. His research concerns the development of semantic models and static analysis tools for the Java language, both at the source and bytecode levels. Eric has also helped in the development of Rice's compiler for the NextGen programming language, an extension of the Java language with generic run-time types. Contact Eric at eallen@cs.rice.edu.

Summary:  In this installment of Diagnosing Java Code, Eric Allen takes a break from examining specific bug patterns and opts instead to discuss the issues involved in designing software that is easy, and even pleasurable, to test. He outlines seven design principles that can greatly increase your productivity in writing tests, and thereby improve the robustness of the resulting code base.

View more content in this series

Date:  01 Sep 2001
Level:  Introductory
Activity:  2745 views

This article is dedicated, humbly, to the victims of Tuesday's attacks, the heroes who have defied this tragedy, and the unbreakable spirit of the American people.

When you design a large program, you must constantly keep in mind the impact of various design choices on features such as performance and extensibility. As a software product becomes increasingly complex and ubiquitously deployed, the "testability" of that software becomes a more important consideration.

The importance of thoroughly testing code is evident. The time and effort spent writing tests and testing code pay off in dramatically reduced maintenance costs.

However, unless you're careful, the effort involved in testing code can mount up to several times the effort of writing the code in the first place! I've seen programmers make concerted efforts to fully cover their code with unit tests, and many of them end up disheartened at how much time it takes.

Fortunately, it doesn't have to be this way. With the application of a few basic principles when you design the software, it is possible to write code that is easy, and even fun, to test.

Like any other set of coding principles, these are not meant to be unquestionable or unalterable dogma. There are times when it is necessary to break the rules. For this reason, it is important to understand the motivations behind each principle and to be able to determine when these motivations don't apply (or are usurped by more important concerns).

Principle 1. Out of GUI view

Move as much code as possible out of the view of a GUI. The various GUI actions can then be simple method invocations on the model. Why do you want to do this?

  • It is much easier to test functionality through method invocations than indirectly, as with GUI testers.
  • An additional benefit is that it makes it easier to modify the functionality of the program without affecting the view.

Of course, there can be bugs in the view, too. Ideally, the tests for a program will check both the model and the view. (For more on testing views, refer to my article on the Liar View bug pattern or read Extreme Programming Installed by Jeffries et al. Both are linked in the Resources section.)


Principle 2. Error check with types

Types are your friends -- use the type system as much as possible to automatically check for errors.

Types can automatically catch a bug in your program before it is run. Without static type checking, a type error may linger in your program as a saboteur until just the right execution path happens to uncover it.

The issue of using types to maximum advantage can be tricky. Often, a collection of data structures can be used together at one level of abstraction, or factored out into a single, higher-level abstraction with a new associated data type.

In fact, the history of programming languages itself can be viewed as a gradual increase in the levels of abstraction at which one can program. Assembly language provided an abstraction over single bits to integers and floating-point numbers. This was followed by such abstractions as records and functions, which was then followed by abstractions such as objects, classes, threads, and exceptions.

At each level of abstraction, functionality identical to the higher-level abstractions could be achieved, but only with substantially more effort and risk of mistake.

In object-oriented languages (as well as other modern languages), the individual programmer is given a great deal of flexibility in devising abstractions. The level of abstraction at which to design a program then becomes a decision based on tradeoffs, such as the added robustness provided by an abstraction level and the expressiveness (and sometimes performance) lost in not working at a lower level of abstraction.

In general, the added robustness and simplicity of higher levels of abstraction are seldom outweighed by other considerations. (For more discussion on this issue, refer to my article on the Impostor Type bug pattern, linked in the Resources section.)


Principle 3. Use mediators to avoid "fault lines"

By "fault lines" I mean the interfaces between separate components that have little interaction compared with the internal interaction of their respective subcomponents. A classic example of such a fault line would be the interface between a GUI view and its model. Other examples include the interfaces between various phases of processing in a compiler or the interface between the kernel and user interface of an operating system.

Find the fault lines of your program, then use mediators with forwarding functions to quickly access aggregate components.

Often, it is easier to test each component along a fault line in isolation. But if there are many objects exposed by each component, or if some of the objects you would like to test in a component are accessible only by following several nested references, testing can become quite tedious.

Instead of testing in isolation, it often helps to have a single mediator object on which you call the various methods you want to test. This object can then forward these method calls to the appropriate places.

Along the same lines, it is useful to design interfaces to program components in tandem with the tests over them. This will focus your efforts on keeping these interfaces as simple as possible.


Principle 4. Methods: Small signatures and default arguments

Using small method signatures and overloading methods with default method arguments will make it much more pleasant to invoke these methods in your tests. Otherwise, you'll have to construct the extra arguments when testing the methods. If the arguments are large, this can quickly lead to code bloat. Even worse, it can tempt you into writing fewer tests than you otherwise would.


Principle 5. Accessors should not modify memory state

Use accessors that do not modify the state of memory to check the state of objects in your tests.

In some respects, tests are like laboratory experiments. They both attempt to verify that particular hypotheses hold. This is much harder to do if the very act of inspection alters the state of the world.

Unlike the world of quantum mechanics, the state of a computer process can be checked without modifying that state. Use this to your advantage.


Principle 6. Specify out-of-program components with interfaces

Using interfaces to specify outside program components allows for easy simulation of these components in the test cases.

This principle can save a tremendous amount of time, especially if implementation of the outside component isn't complete. All too often, the most essential components aren't available on time. If you can't test your own code without these components in place, you are headed for disaster. Your clients won't care that you only had two hours to integrate with a two-week-late component. All they know is that the integrated product is late and that it's broken.


Principle 7. Tests should come first

Write the tests first. This is standard XP practice, but it is always tempting to ignore it.

Every time I succumb to this temptation, I regret it. Given that you're trying to produce correct code, the time you appear to save by postponing the writing of tests is nothing but an illusion.

Note: This doesn't mean that you should write the entirety of the tests in one shot, followed by the entirety of the implementation. It is better to write a few tests, implement them, write a few more, implement those, and so on. In this way, the design evolves; oversights are caught during the implementation phase and corrected in the next set of tests. It is also less daunting to write the tests in this way.


More code than you need?

With a little effort, any program can be tested thoroughly and easily. Of course, it is inevitable that there will be cases where these principles don't apply; then, it will seem impossible to test functionality.

When such cases occur, I try to take a step back from the question, "How can I possibly test this kind of code?" Instead, I ask myself, "How could I write this code in such a way that I can test it?" This change in thinking will often result in the addition of many pieces of functionality that serve no purpose other than to facilitate the testing.

What? Don't worry; when that happens, it's perfectly okay.

Just as many of the existing design patterns add multiple classes to a program (such as visitors, decorators, and such) that are necessary solely to add extensibility to the program, it is acceptable to develop new patterns to facilitate testing. Indeed, many of the features of an object-oriented language are included to facilitate extensibility; why shouldn't future versions of the language (or entirely new languages) include features to facilitate testing.

In the case of the Java language, this is already beginning. Future versions are scheduled to include much more powerful type systems, assertions, and the like. Just as object-oriented languages have increased the degree to which we can reuse and extend existing code, future, test-oriented designs and features will help to increase the robustness of our code, both old and new.


Resources

  • The Division of Software Engineering at DePaul University has done some work on automated theorem provers to detect null-pointer exceptions in Java.



  • The JUnit home page provides links to many interesting articles discussing program testing methods, as well as the latest version of JUnit.



  • Roy Miller and Chris Collins offer "XP distilled" (developerWorks, March 2001), an article that illustrates how Extreme Programming methods can deliver greater success on your Java projects.



  • Check out the official XP programming rules at ExtremeProgramming.org.



  • For some ideas on how to test views, try Eric Allen's article on the Liar View bug pattern (developerWorks, April 2001).



  • For a rationale of why using higher levels of abstraction is more rewarding (even if it is more work up front), see Eric Allen's article on the Impostor Type bug pattern (developerWorks, July 2001).



  • Find more Java resources on the developerWorks Java technology zone.

About the author

Eric Allen has a bachelor's degree in computer science and mathematics from Cornell University and is a PhD candidate in the Java programming languages team at Rice University. Before returning to Rice to finish his degree, Eric was the lead Java software developer at Cycorp, Inc. He has also moderated the Java Beginner discussion forum at Javaworld. His research concerns the development of semantic models and static analysis tools for the Java language, both at the source and bytecode levels. Eric has also helped in the development of Rice's compiler for the NextGen programming language, an extension of the Java language with generic run-time types. Contact Eric at eallen@cs.rice.edu.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10585
ArticleTitle=Diagnosing Java Code: Designing "testable" applications
publish-date=09012001
author1-email=eallen@cs.rice.edu
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers