Implementing Assertions for Java

Assertions act like watchdogs that assist you in finding bugs earlier in the development process. Our authors show how you implement assertions for Java.


January 01, 1998
URL:http://www.drdobbs.com/jvm/implementing-assertions-for-java/184410464

Jeffery is president and CEO of Reliable Software Technologies (RST). Michael and Matthew are developers in RST's research division. The authors can be contacted at http://www.rstcorp.com/.


Sidebar: Design by Contract

Debugging is hard work. Even more frustrating is when the bug turns out to be something that could easily have been prevented or detected. Perhaps you misunderstood the specification or design. Maybe your testing wasn't adequate. Or maybe you just weren't thinking clearly at one a.m. before that big delivery deadline. Regardless, you wasted hours or days tracking down a problem that should never have existed. How can you avoid this in the future?

Software assertions combat this problem. Assertions are Boolean expressions that define the correct state of your program at particular locations in the code. Think of them as watchdogs that check method calls for proper invocation, method code for correct computation, class data states for consistency, and individual statements for errors.

The format of an assertion typically looks like <assertion_type>(<condition>, <message>);, where <assertion_type> indicates the purpose of the assertion, <condition> indicates the Boolean expression evaluated to determine whether the assertion has been violated, and <message> indicates what information should be output if the assertion is violated.

There are many assertion types, including:

Software assertions can assist you in finding bugs earlier in the process (when they are easier to fix). As developers move toward object-oriented design and component reuse, concepts such as design by contract (see the accompanying textbox entitled "Design by Contract") use assertions to assure proper implementation of classes, component interface, and internal data states. Many believe that assertion technology is the key to designing for testability -- the holy grail of software quality. Perhaps these checks can be used to move toward built-in self-test (bist) as a way of automatically checking the consistency and correctness of our implementations.

Unfortunately, the developers of Java did not include assertion capabilities in the language. We are thus left to roll our own assertion capabilities in order to use design by contract and assertions effectively. The developers of Java also decided not to include a preprocessing capability in the language. Since many assertion capabilities developed for other languages have used their preprocessors to automatically enable/disable the assertions, this makes providing support for assertions in Java more difficult.

In this article, we'll discuss the issues associated with building assertion capabilities for Java. We'll also discuss the ideal Java assertion capability and our implementation of this ideal. Finally, we'll present a basic assertion class that you can use.

Ideal Java Assertion Capabilities

Assertions should be powerful, easy to write, easy to read, and easy to use. The language used to specify assertion statements must provide a simple means of expressing complex Boolean conditions. It must be easy for the programmer or tester to write and understand these expressions. A framework that allows users to easily manage assertion statements in source code must exist.

Part of an assertion tool's usefulness will come from the ability to configure the tool to the task at hand. Users may require different capabilities during development phases of a project than during testing or release phases. One example of this is the ability to decide which assertions should be evaluated by the program. Preconditions, postconditions, invariants, and data state assertions should be capable of being toggled on or off independently. Users should have the choice of making this decision at either compile time or run time. Additionally, users should have the option of having any or all of the types of assertion statements omitted from compilation.

A good default behavior for when an assertion fails is to terminate the program. When an assertion has failed, it means that the program has entered an incorrect state and may no longer function properly. However, this does not necessarily have to result in the termination of a program. Ending the program is just one of several useful behaviors that can be built into an assertion tool. Other behaviors include putting the program into a suspended state or allowing the program to continue as though nothing has happened. Each of these behaviors can be useful in different situations. Pausing a program would allow you to load it into a debugger and examine the state of the program. Allowing a program to continue after the failure of an assertion might be used to examine the propagation of an error through the rest of that program. A useful assertion tool would provide a variety of behaviors, and these behaviors would be configurable at either run time or compile time.

Another aspect of an assertion's behavior that users should be able to configure is the destination of the assertion's output. There are times when output should be sent to stdout, stderr, a file, or even a pop-up window. Like the other behaviors, this should also be configurable at either run time or compile time.

The placement of assertion statements (preconditions, postconditions, and invariants) is crucial to the readability of the program. Java assertions must be placed in the class definition section of a program. Placing preconditions and postconditions at the beginning of a class method makes them easy to identify. Although the beginning of a method is the proper place to evaluate a precondition, postconditions must be treated differently. Postconditions need to be evaluated before each exit point in the method. For example, a method that has multiple return statements should evaluate the postcondition before each of these return statements. Ideally, users should be able to place the postcondition at the top of the method and have the assertion tool evaluate it at all of the appropriate locations. This helps keep the program readable and adds to the usefulness of the assertion.

Invariants need to be treated in a similar manner. A Java programmer would like the ability to specify a class invariant once in the class definition, then have it evaluated at the entry and exit of every public method for that class. A Java assertion tool should provide you with the ability to place preconditions, postconditions, and invariants at convenient locations in the class definition, and still evaluate them at the proper locations in the code.

A useful assertion tool will have to provide extensions to Java's Boolean expression syntax. Boolean expressions used in assertion statements should be easier to read and provide more functionality than ordinary Java Boolean expressions. The means of specifying Boolean expressions for assertion statements will be referred to as the "assertion language." The assertion language should include functions like the existential quantifiers "for-all" and "there-exists." This provides users with a simple means of expressing complex Boolean expressions that are difficult to write using traditional Java expressions. The tool should provide other functions (perhaps Range and Equals functions), as well as tokens to make Java expressions more readable. An example of such tokens would be to allow use of the words "AND" and "NOT" to respectively replace "&&" and "!". The more functions an assertion language provides, the easier it should be to write powerful yet readable Boolean expressions.

There are a couple of metavariables that the assertion language should provide that are unique to a postcondition statement. Postcondition statements should have a way to refer both to the return value of the function and to the initial value of any of the function's arguments. Providing these capabilities will add a great deal of flexibility to postcondition statements, and allow them to specify conditions that they would otherwise be unable to express. For example, Listing One is code for a class written using Pajama (RST's assertion tool). Note that Pajama uses special comment blocks to allow assertions to be toggled on and off during development and testing. It supports automatic placement of preconditions and postconditions. It also provides a full-featured assertion language.

Implementation Issues

The biggest obstacle to implementing your own Java assertions is the lack of a preprocessor. A preprocessor can be used to provide the ability to compile programs without including the assertions that are embedded in the source code. The only way to implement this optional compilation ability in Java is to either add assertion support directly to the Java compiler, or write a specialized preprocessor. Writing a script that goes through a program's source code and places all assertions in comments (or pulls them out of comments) could provide this functionality.

Java's lack of system calls makes it difficult to modify the behavior of an assertion at run time. The only way to pass parameters into the assertion library at run time is via the Java Virtual Machine (JVM) system parameters. The type of behavior that you want when an assertion fires depends on how your code is being used. Typically, you want your program to terminate when an assertion is fired during the debugging process because the violation of an assertion indicates that a bug exists somewhere in the program. Java, however, makes implementing even this simple behavior more difficult than it first appears. The assertion tool must be able to recognize that a Java program could be an applet (which can't terminate) and execute some behavior other than termination.

Properly dealing with inheritance requires that when a precondition or postcondition of a polymorphic method is evaluated, the precondition or postcondition in the superclass' corresponding method may also have to be evaluated. This means that an assertion needs to be duplicated in both the superclass and derived classes. Duplicating the assertion can be done by the JVM, Java compiler, or perhaps an advanced preprocessor. Using the JVM would involve adding the ability for it to look in the superclass for the appropriate assertions and then evaluate them at run time. The preprocessor or Java compiler could be used to copy the assertions from the superclasses to the derived class at compile time. To perform this copying at compile time, the compiler would have to be able to identify the assertions in the .class file of the superclass without relying on the .java file.

A postcondition should have the ability to refer to the value that a variable had at the beginning of a method. Java adds a level of complexity to this because, in Java, every assignment is made by reference. For example, if the statements in Example 1 are executed in a C++ program, x is left with the value 10, and old_x is left with the value 5. If, however, these statements are contained in a Java program, then x finishes with the value 10, and old_x also has the value 10. This is because old_x has been assigned to be a reference to x. Java requires that copy semantics are used to correctly implement the "old" function.

Rolling Your Own Assertions

Listing Two is a simple assertion tool that supports PreCondition, PostCondition, and Assert statements. When an assertion is violated, a message is printed and a stack trace of the program is displayed. The stack trace is useful for locating the bug in the program. This assertion tool doesn't support many of the features an assertion tool should (such as a full-featured assertion language and optional compilation). However, it provides the basic capabilities necessary to begin using assertions in your code.

The assertion tool provides the methods PreCondition, Assert, and PostCondition. These methods should be used to check the correctness of input to a method, internal state of a method, and output of a method, respectively. These methods all accept a Boolean condition and a message as their two parameters. The Boolean condition is the condition that is being asserted to be True. If the condition does evaluate to True, the method simply returns. If the condition is False, then the message is printed along with a stack trace (determined by the printStackTrace method). The function System.exit() is called to terminate the program after it has reached an internally corrupt state.

As Listing Three shows, this example tool provides basic assertion support and can be modified to better fit into your development environment. If you want to provide behaviors other than termination when the assertion fires, replace the System.exit() call with code that performs the desired behavior. If you have a GUI-based application or applet, you may want to replace the System.out.println() calls with calls that will display the message to a pop-up window. In the case of applets, this can also display a warning to users stating that continuing may have unpredictable behavior.

Conclusion

Assertions provide a powerful mechanism for assisting you in the creation of Java classes and integration of Java components. Ideally, assertions would be part of the Java language. Since they aren't, we've provided a basic assertion capability and presented information on what a more powerful assertion capability should include.

DDJ

Listing One

public class math{ 
private static int double_it(int x)		// doubling algorithm
  {
    /*+ POST_CONDITION( $RETURN = 2 * $OLD(x), "didn't double correctly"); +*/
    x *= 3;		// bug in method
    return x;
  }
public static int sqrt(int x)		// square root algorithm
  {
    /*+ PRE_CONDITION(x >= 0, "Can't take the sqrt of a negative number"); +*/
    int i;
    for (i=0;i*i <= x; i++)
      {
      }
    return i-1;
  }
public static void switcheroo(char c)	// prints a gender
  {
    switch ( c )
      {
      case 'm' :
      case 'M' :  System.out.print("Male");  break;
      case 'f' :
      case 'F' :  System.out.print("Female");  break;
      default :  /*+ ASSERT(false, "Not a valid gender!");  +*/  break;
      }
  }
public static void main(String argv[])	// test method for class
  {
    /*  These all work:  */
    System.out.println("2 * 0 = " + double_it(0));
    System.out.println("sqrt(9) = " + sqrt(9));
    switcheroo('m');
    System.out.print(" ");
    switcheroo('f');
    System.out.println();
    /*  These all fail */
    System.out.println("2 * 3 = " + double_it(3));
    System.out.println("sqrt(-9) = " + sqrt(-9));
    switcheroo('q');
  }
} 
}

Back to Article

Listing Two

import java.io.*;public class Assert
{
private static String getStackTrace()	// gets a stack trace
  {
    Throwable t = new Throwable();		// for getting stack trace
    ByteArrayOutputStream os = new ByteArrayOutputStream();
				// for storing stack trace
    PrintStream ps = new PrintStream(os);	// printing destination
    t.printStackTrace(ps);
    return os.toString();
  }
public static void PreCondition(boolean condition, String message)
			           // code to check PreConditions
  {
    if (!condition)	// was PreCondition violated?
      {
        System.out.println("Precondition [" + message + "] fired at:");
				// print user msg
        System.out.println(getStackTrace());	// print stack
        System.exit(1);
      }
  }  
public static void Assert(boolean condition, String message)
			           // code for data state assertions
  {
    if (!condition)	// was assertion violated?
      {
        System.out.println("Assert [" + message + "] fired at:");
        System.out.println(getStackTrace());
        System.exit(1);
      }
  }  
public static void PostCondition(boolean condition, String message)
			           // code for PostCondition check
  {
    if (!condition)	// was PostCondition violated?
      {
        System.out.println("Postcondition [" + message + "] fired at:");
        System.out.println(getStackTrace());
        System.exit(1);
      }
  }  

Back to Article

Listing Three

public class math		// doubling algorithm{ 
private static int double_it(int x)
  {
    int ret = x*3;  // bug
    Assert.PostCondition(ret == 2*x, "didn't double correctly"); 
    return ret;
  }
public static int sqrt(int x)		// square root algorithm
  {
    Assert.PreCondition(x >= 0, "Can't take the sqrt of a negative number"); 
    int i;
    for (i=0;i*i <= x; i++)
      {
      }
    return i-1;
  }
public static void switcheroo(char c)	// prints a gender
  {
    switch ( c )
      {
      case 'm' :
      case 'M' :  System.out.print("Male");  break;
      case 'f' :
      case 'F' :  System.out.print("Female");  break;
      default :  Assert.Assert(false, "Not a valid gender!"); break;
      }
  }
public static void main(String argv[])	// test method for class
  {
    /*  These all work:  */
    System.out.println("2 * 0 = " + double_it(0));
    System.out.println("sqrt(9) = " + sqrt(9));
    switcheroo('m');
    System.out.print(" ");
    switcheroo('f');
    System.out.println();
    /*  These all fail */
    System.out.println("2 * 3 = " + double_it(3));
    System.out.println("sqrt(-9) = " + sqrt(-9));
    switcheroo('q');
  }


Back to Article


Copyright © 1998, Dr. Dobb's Journal

Dr. Dobb's Journal January 1998: Implementing Assertions for Java

Implementing Assertions for Java

By Jeffery E. Payne, Michael A. Schatz, and Matthew N. Schmid

Dr. Dobb's Journal January 1998

 . . .
Coordinate x, old_x ; 
x = new Location() ;
x.setValue(5);
old_x = x ;
x.setValue(10) ;
 . . .

Example 1: In C++, old_x has the value 10; in Java, 5. (Coordinate is a user-defined type.)


Copyright © 1998, Dr. Dobb's Journal

Dr. Dobb's Journal January 1998: Design by Contract

Dr. Dobb's Journal January 1998

Design by Contract


Design by contract is a technique used to help ensure the correctness of software. A software contract is a specification of the behavior of a class and its associated methods. The contract outlines the responsibilities of both the caller (client) and the method being called. Failure to meet any of the responsibilities stated in the contract results in a breach of contract, and indicates the existence of a bug somewhere in the implementation of the software. Contracts increase the reusability and robustness of software while decreasing its complexity. Correctly implemented software contracts reduce the chance that software bugs could remain unnoticed during the testing of a program.

There are three elements essential to defining a software contract: preconditions, postconditions, and invariants. Preconditions and postconditions define the responsibility of the client and method, respectively. Invariants define rules common to both the client and the methods. Defining responsibilities for classes and methods helps you to avoid writing redundant checks because methods and classes are contractually bound to fulfill their part of the contract.

A method's preconditions represent the responsibilities that a client has when making a call to that method. Preconditions specify what a portion of a program's state must be at the entry of a method. If a method's preconditions are violated, then this is a breach of contract, and the method is freed from the responsibility of meeting its postconditions. It is the responsibility of the client to meet a method's preconditions, not the responsibility of the method to verify that its preconditions have been met.

Postconditions are used to specify a method's responsibilities. As long as its preconditions are satisfied, a method is responsible for leaving the program in a state defined by its postcondition. It is the method's responsibility to ensure that this state is reached, and the client can continue executing while safely assuming that the method has performed correctly.

Unlike preconditions and postconditions, invariants are universal across an entire class. An invariant specifies both the responsibilities that a client has for using a class, and the responsibilities that the class's methods have to the client. An invariant is equivalent to supplying a common condition as both a precondition and postcondition for each public method of a class. Meeting the requirements set by an invariant is the responsibility of the client when calling a method and the responsibility of the method before returning to the client.

A software contract can be completely specified through the use of preconditions, postconditions, and invariants. Specifying contracts in this manner produces classes that are easy to use and understand. The contract serves as a guide to using a class as well as a formal specification of the behavior of the class. Contracts are therefore useful during both the design and implementation phases of a project. Design by contract produces a class whose behavior is well defined and can therefore easily be reused in the future.

-- J.E.P., M.A.S., M.N.S.


Copyright © 1998, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.