Chapter 10. Developing Robust Programs Leo Tolstoy began his classic Anna Karenina with the statement “Happy families are all alike; every unhappy family is unhappy in its own way.” So far in this text, we have made it a general practice of assuming “happy” scenarios – our users always behave as expected, programmers always use our APIs appropriately, and the files we read always contain properly-formatted data. In Chapter 7 for example we assumed that no user would ever try to enter the string “forty” (rather than the digits “40”) as a frame rate, in Chapter 9 we assumed that no programmer would ever try to construct a temperature below absolute zero, and in Chapter 8 we assumed that the country names stored in the input files never have spaces in their names, e.g., “United States”. These assumptions are hopelessly naïve. As Tolstoy observed, for every happy scenario there are myriad unhappy scenarios. We know that users can and probably will break every assumption our code makes, either purposely or accidentally. We know that programmers who don’t understand our class APIs may use them improperly; truth be told, we acknowledge that over time even we forget how to use our own APIs properly. We know that the content of data files is notoriously inconsistent and incomplete. In addition to ignoring problems related to the use of our code, we have also largely ignored the problem of testing our code. We have, for the most part, developed our applications, run them a few times to see if they seem to be working and then moved on to the next programming task. As our applications grow increasingly complex, the viability of this ad hoc approach to testing decreases alarmingly. There are simply too many things to test when we write code and too many subtle problems that can be caused each time we modify it. Our goal is to develop correct programs, which means that our programs must produce appropriate behaviors in both the happy scenarios and the unhappy scenarios. Fortunately, the Java development environment provides tools that help us with this task. It provides unit testing tools to help ensure that our programs produce correct results in the happy scenarios, and exception handling tools to help ensure that our programs respond appropriately in the unhappy scenarios. This chapter discusses exception handling in Java and then unit testing using JUnit. It also introduces the notion of user testing for the purpose of ensuring the usability of our interactive applications. Finally, it introduces enumerated types as a way to make our programs easier to understand and therefore easier to modify and maintain correctly. 10.1. Example: Temperature Conversion Some computing applications must work with temperature measurements, each of which may be represented using a different measurement scale, e.g., Celsius, Fahrenheit or Kelvin. To support such applications, Chapter 9 created a Temperature class that encapsulates the key features of temperature objects. Each temperature object knows its own magnitude in degrees (e.g., 98.6) and the scale in which it is represented (e.g., Fahrenheit) and it knows how to convert its magnitude from one scale to another. 10-1 This chapter focuses on maintaining the integrity of the temperature objects constructed by our class so that they behave in a robust, correct manner in all situations. In Chapter 9, we included some preliminary code that rejects attempts to create invalid temperatures, say temperatures below absolute zero or temperatures specified in unknown scales, but that code was heavy-handed, using System.exit() to terminate the program entirely if any problem, large or small, was found. Java provides better mechanisms for handling these issues more even-handedly. In Chapter 9, we also attempted to ensure that the temperature class performed scale conversions properly. For example, when we created a 0.0○ Celsius temperature object and asked it to convert itself to Fahrenheit, we expected the result to be a 32.0○ Fahrenheit temperature object (because we know that 0.0○ C = 32.0○ F). We tried to ensure the correctness of these conversions by manually running a set of test cases, e.g., 0○ C = 32○ F , 100○ C = 212○ F , etc, but this proved to be both tedious and error-prone. There are too many test cases to remember and they need to be run every time we change the implemention of the temperature class. Instead, we need to be more systematic in our testing. Given the range of temperatures that we could represent, it would be impractical to test every possible case and every possible conversion. However, we need to test a sufficient range of cases to assure ourselves that the system can handle anything that it will see in a production environment and we would like to automate this testing so that it is easy to run and re-run whenever we modify our temperature class definition. Java provides mechanisms for doing this as well. To illustrate these things, this chapter will reuse the temperature class code from Chapter 9 and construct a Enter a temperature: 98.6 F simple console application for temperature conversion Convert to: K that will look something like the example shown in Figure Result: ?? 10-1. This application should allow the user to enter a temperature value in any of the scales and then should automatically convert the value into the desired scale. Our Figure 10-1. A simple temperature converter utility class should resist any attempt to construct objects representing temperatures below absolute zero, and it should perform scale transformations correctly in all legal cases. The figure shows one happy scenario in which the user enters 98.6○ Fahrenheit, which we know to be a legal temperature, and the system should correctly convert that value into the appropriate Kelvin equivalent. At this point, we can’t remember what that Kelvin value should be, but we’ll figure that all out when we specify the formal tests. This chapter focuses less on the development of the class itself, which was discussed at length in Chapter 9, but focuses instead on the development of a robust temperature class that gives correct results in happy scenarios and responds appropriately in unhappy scenarios. 10.2. Exception Handling This section discusses Java tools for anticipating and handling unhappy scenarios. In particular, it discusses Java’s exception handling mechanism. 10-2 10.2.1. Working with Exceptions When the Java environment encounters a fault of some sort during the execution of a program, it creates an object representing that fault. These objects are called exceptions. Java creates exceptions in a number of different situations. For example, the following code segment attempts to print the result of 1/0: System.out.println(1/0); In this unhappy scenario, we’ve attempted to divide by zero, which we know to be undefined. The Java environment prints the following message on the text output console: Exception in thread "main" java.lang.ArithmeticException: / by zero at c10quality.TemperatureConsole.main(TemperatureConsole.java:13) This output indicates that a division-by-zero exception has occurred at line 13 of the main() method defined in TemperatureConsole.java. In this case, we say that Java has thrown an ArithmeticException. When Java throws an exception like this, it does not automatically terminate the program. We can write code to deal with this exception in some appropriate manner, say by printing an alternate message on the screen. This is called handling an exception, and we discuss the flexibility of this technique below. For our temperature class, consider what happens if the programmer attempts to construct a temperature object whose value is below absolute zero (e.g., -1.0° Kelvin)? In Chapter 9, we added code to the explicit-value constructor to deal (heavy-handedly) with this case: public Temperature(double degrees, char scale) { if (isValidTemperature(degrees, scale)) { myDegrees = degrees; myScale = Character.toUpperCase(scale); } else { System.err.println("Invalid Temperature: (" + degrees + "," + scale + ")"); System.exit(-1); } } If a programmer attempts to create valid temperature object, everything works as normal. If, on the other hand, the programmer attempts to create a sub-absolute-zero temperature object, this code prints an error message and exits the application. This implementation had the benefit of enforcing our established invariant, which was good enough for our purposes in Chapter 9, but it puts the Temperature class in charge of what happens to the application; the TemperatureConsole application therefore loses control over the application. This is not a good division of responsibilities. The Temperature class should control the integrity of each temperature object and the TemperatureConsole should control what happens with the application. In this scenario, it would probably be preferable to keep the application running and to print a message instead, warning the user gently about the evils of subabsolute-zero temperatures. An alternative approach might be to program TemperatureConsole to handle sub-absolute-zero temperatures before even trying to construct one using the temperature explicit-value constructor. This 10-3 could work, but would require that TemperatureConsole know about what makes a temperature valid or invalid, which puts the TemperatureConsole in charge of Temperature objects. This is not a good division of responsibilities either. It’s difficult and unnecessary for the TemperatureConsole to know about the temperature class’s established invariants; these are things best hidden inside the Temperature class. A better option would be to: (1) program the Temperature to determine if its invariants have been violated and to alert its calling program, here the TemperatureConsole, whenever that happens; and (2) program the TemperatureConsole to listen for the alerts and manage the application appropriately when they happen. This puts the temperature class in charge of the integrity of temperature objects and the temperature console in charge of the application, which is the correct division of responsibilities. Though we cannot easily implement these behaviors with the control structures we have discussed so far in this text, Java provides an exception handling mechanism that supports both of them. The next two sections discuss each of these behaviors in turn. 10.2.2. Throwing Exceptions The temperature class’s explicit-value constructor can “alert” its calling program to problems by constructing and throwing an exception object as follows: public Temperature(double degrees, char scale) throws Exception { if (isValidTemperature(degrees, scale)) { myDegrees = degrees; myScale = Character.toUpperCase(scale); } else { throw new Exception("Invalid Temperature: " + degrees + " " + scale); } } If the received values are valid, then this code constructs the specified temperature object. If, on the other hand, the received values are invalid, this code constructs a new object of type Exception and “throws” it back to the calling program. The constructor for the Exception class receives a string that should specify a useful message indicating the nature of the exception. The throw command terminates the execution of the method and returns control back to the calling program. Methods that throw Exception objects must add the throws Exception clause to their declaration. In this case, the exception indicates that the constructor has received an illegal temperature specification (“Invalid Temperature:…”). Given this new definition of the explicit-value constructor, programmers that construct new temperature objects must handle the thrown exception as discussed in the next section. The example uses a generic Exception object. Java supports a variety of specialized exception types, modeled as a hierarchy shown here: 10-4 This hierarchy includes as its root the Exception type used above. It also includes some exception types that you may have encountered in other circumstances, including the NumberFormatException, thrown when the Integer class tries to read non-digits as an integer (e.g., when scanning “forty” rather than “40”), the IndexOutOfBoundsException, thrown when indexing beyond the end of any array, and the NullPointerException, thrown when Java tries to use an object handle that has not been initialized. Though it is generally a good idea to use the most specific exception type appropriate for the given situation, this text adopts the simplifying practice of using Exception, the most general exception type. As shown above, throwing an exception of type Exception requires that the method declare that it might throw an exception. This is called a checked exception because the throwing method must specify a throws declaration and the calling program must handle the exception. It is generally a good idea to be explicit about exceptions in this way, but it does require extra work of the programmer. To help ease the burden somewhat, Java also provides unchecked exceptions, which do not require the throws declaration or exception handling. The RuntimeException type and its children are unchecked exceptions. If a calling program does not handle an unchecked exception, then that calling program also terminates and returns control back to its calling program, and so forth. This propagation continues until some code handles the exception. Ultimately, the Java runtime environment will handle the exception by printing the error message to the output console, as shown in the previous section.1 The pattern for using the throw statement is given here. Throw Pattern throw ExceptionObject; ExceptionObject is an object of some type in the Exception class hierarchy; checked exception types must add a throws clause to the method declaration line. 1 A discussion of the checked-unchecked exception issue can be found here: http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html. 10-5 10.2.3. Handling Exceptions throw statements are an alternate form of the return statement in that they return control back to the calling program, but a throw must be handled differently than a simple return. The calling program must handle a thrown error using a try-catch block. try { Temperature temperature1 = new Temperature(-1.0, 'k'); } catch (Exception e) { System.out.println(e.getMessage()); } The try{} block contains calls to methods that might throw exceptions. In this example, the call to the temperature explicit-value constructor may or may not throw an exception depending upon whether it likes the arguments it receives. The catch{} block contains the statements that respond to any exception that is raised and “receives” the exception that it is to handle using a construct similar to a parameter list, i.e., (Exception e). In this example, the TemperatureConsole attempts to create a -1° Kelvin temperature, which causes the Temperature class to throw an exception. The try-catch block of the console class catches the exception, giving it the identifier e, and accesses the error message specified by the constructor using the getMessage() method on the exception object e. The output is shown here: Invalid Temperature: -1.0 K The flow of control looks something like this: Control starts in the temperature console application code (on the left), proceeds as normal to the call to the explicit-value constructor, which passes control to the definition of the constructor in the temperature class (on the right). Because the temperature is invalid, the constructor executes its else clause, which constructs and throws an exception object back to the calling program. The catch clause receives the thrown exception object and prints out the message contained therein. If no statement in the try block throws an exception, then control proceeds to the end of the try block, skips the catch{} block and goes on to the statements following the try-catch blocks. If any statement in the try block does throw an exception, then Java looks for a corresponding catch block that specifies the type of exception that was thrown (or one of its parents in the exception hierarchy) and 10-6 transfers control to that block. In the example given above, the method throws an exception of type Exception and the catch block specifies the same type Exception. Thus, control passes from the method call that threw the error directly to the catch block, skipping any as-of-yet unexecuted statements in the try block. Each try-catch block has one try block that may contain multiple statements that could throw exceptions. Each try-catch block must have at least one catch block, but it can have multiple catch blocks each handling a different type of exception. Consider the following code, which may raise and handle two different types of exceptions: Temperature temperature; Scanner scanner = new Scanner(System.in); System.out.print("Please enter a temperature: "); try { temperature = new Temperature(scanner.nextDouble(), scanner.next().charAt(0)); System.out.println("You entered: " + temperature); } catch (InputMismatchException e) { System.out.println("Please enter a valid degree value."); } catch (Exception e) { System.out.println(e.getMessage()); } In this code, the try block contains several statements that could throw exceptions: the temperature class’s explicit-value constructor, as we programmed it above, can throw an exception of type Exception if the temperature values are not valid; nextDouble() can throw an exception of type InputMismatchException if the value it is reading is not a valid double number. This code also includes two catch blocks, each handling its particular exception in its own way. Note that the ordering of these two catch blocks is important. We should handle more specific exception types before handling more general types. For example, because InputMismatchException is a special type, that is, it is below Exception in the exception hierarchy, we must catch it first, before catching the more general Exception; otherwise, Java treats the InputMismatchException as an Exception and generates an error indicating that control can never reach the trailing InputMismatchException catch block. The try-catch pattern is as follows: 10-7 try-catch Pattern try { Statements } catch (ExceptionClass1 exception1) { ExceptionHandlerStatements1 ... } catch (ExceptionClassn exceptionn) { ExceptionHandlerStatementsn } [finally { CleanupStatements }] Statements is a block of statements each of which may throw exceptions; ExceptionClassi exceptioni is a parameter list specifying a class from the Exception hierarchy and an associated identifier; ExceptionHandlerStatementsi are statements that handle exceptions of type ExceptionClassi; there can be as many catch blocks as required; finally { CleanupStatements } is a set of statements designed to be executed after the chosen catch block, regardless of which block that is. The finally clause is optional (as indicated by the “[ ]” given in the pattern) and is commonly used to cleanup file processing commands. 10.2.4. Revisiting the Example Given the exception handling techniques discussed in this section, we can extend the temperature class defined in Chapter 9 with a more effective means of establishing and maintaining its invariant. The upgraded version of the explicit-value constructor is discussed in the previous section. The additional code required to upgrade the Temperature class is shown in this section. The upgraded version of the change() method is as follows: public void change(double amount) throws Exception { double newDegrees = myDegrees + amount; if (isValidTemperature(newDegrees, myScale)) { myDegrees = newDegrees; } else { throw new Exception("Invalid Temperature change: " + newDegrees + " " + myScale); } } public void setScale(char scale) throws Exception { 10-8 if (Character.toUpperCase(scale) == 'C') { convertToCelsius(); } else if (Character.toUpperCase(scale) == 'F') { convertToFahrenheit(); } else if (Character.toUpperCase(scale) == 'K') { convertToKelvin(); } else { throw new Exception("Invalid Temperature scale: " + scale); } } These two mutators must be as careful with the invariant as the explicit-value constructor. The change() mutator throws an exception for invalid temperatures just as the constructor does; the setScale() mutator throws an exception if the scale is invalid. The copy() method must specify the throws Exception clause, as shown here: public Temperature copy() throws Exception { return new Temperature(myDegrees, myScale); } Because copy() doesn’t do anything that could invalidate the invariant, it does not, itself, create new exceptions to be thrown, but it does pass along errors thrown by the explicit-value constructor it calls. In this case, Java requires that copy() also specify the throws Exception clause, which indicates that it or something it calls could throw an exception. This same thing is true for all the comparison methods, all of which call this new version of the copy() method. This upgraded version of the temperature class guards all the points at which the data items are either initialized or modified. This is one of the reasons that we’ve maintained the practice of declaring local data items to be private because this forces other code to use our carefully designed constructors and mutators rather than allowing them to directly set data items in an unrestrained manner. 10.3. Functional Testing If part of our goal as programmers is to produce code that is correct, i.e., that it does what it is supposed to do, then we should consider the question of how to test that we have achieved this goal. In some design contexts, it may be difficult to specify what the correct behavior of a program should be. For example, in Chapter 2 it would have been difficult to specify a “correct” poster layout; so many layouts could work and it would be difficult to distinguish them in terms of correctness. In other contexts, however, our notion of correctness can and should be carefully specified and tested. For example, a basic tool like our Temperature class could potentially be used in safety-critical applications where correctness is absolutely critical such as life-support monitors or flight control systems. To help ensure that our Temperature class is up to this level, our testing should follow these principles: Testing should be systematic, addressing the full range of scenarios in which our code might find itself; Testing should be done early and often – the sooner we find an error, the more quickly and efficiently it can be corrected; 10-9 To achieve these goals, we should make testing part of our development process, and, as far as possible, write our code in a way that facilitates systematic and frequent testing. This section discusses Java tools that support these goals. 10.3.1. Specifying Test Cases Good testing is based on good test cases. The analysis phase of any software development project should specify a set of test cases that is sufficient to demonstrate the correctness of any implementation of our analysis and design. These test cases should be built systematically and should specify concrete instances of a system input along with the correct system output. For example, one good test case for our temperature class is that it should convert 0 Celsius to 32 Fahrenheit. It’s concrete and we know the input and the correct result. It is clear that we need to run more than one test case before we are confident that our program is correct. Our program should handle a variety of conversions, in different directions, with positive and negative values, and it should also handle the exception conditions discussed in the previous section. It’s also clear that we can’t generally test all possible cases. Even in the temperature conversion case, we wouldn’t have time to test that all possible real values for Celsius are converted properly to corresponding values in Fahrenheit and Kelvin and vice-versa. Fortunately, it is sufficient to specify a subset of all the possible test cases that are representative of the behavior required of our program. This set should include a variety of test cases that cover all the desired functionality of the system; in our example we’d want to test temperature object construction and scale conversions in all possible directions. This set should also include cases that pay particular attention to extreme conditions, often known as the boundary conditions; in our example, we’d want to test cases at or around absolute zero, cases in which the scale settings are close but not correct, and potentially very large temperature values. For the temperature class, we might want to specify the following list of test cases: Freezing and boiling points: o 0.0° C = 32.0° F = 273.15° K (Let this be short-hand for the two individual test cases: 0C should convert to 32F and 32F should convert to 273.15K.) o 373.15° K = 212.0° F =100.0° C Extreme temperatures: o -273.15° C = -459.67° F = 0.0° K o 1799540.33° F = 999726.85° C =1000000.0° K Exception conditions o -1° K results in thrown error o -460° F results in thrown error o -274.0° C results in thrown error While this is not an exhaustive list, we would be justified in being reasonably confident in the correctness of our temperature class if it handled all these cases correctly. 2 2 We assume here that the range of the double type is sufficient for all possible applications; further treatment of the rather extreme cases that raise this issue is beyond the scope of this text. 10-10 As previously mentioned, it is a good idea to specify the test cases during the analysis phase. You don’t need to know the design of the system to specify these test cases, and it might even be dangerous to know the design or implementation given that you might be tempted to create test cases that you know will work. It is also a good idea to have people other than the programmers specify the test cases, just to make sure that the test cases don’t manifest the same misconceptions that the programmers might have about the requirements. Specifying test cases such as these is the first step in testing the functionality of the application, commonly called functional testing. Note that we’ve focused here on testing the function of the temperature class constructors and mutators rather than on the converter’s user interface. As we’ll see in a later section, testing the usability of a user interface requires a different sort of testing, commonly called usability testing. 10.3.2. Automating Unit Testing Once specified, there are a number of ways that we might run the set of tests, sometimes called a test suite. If we had an interactive user interface of some sort, we could run them by hand. For example, we could fire up the temperature console program and go through each test case manually as we did in the previous section for our first test case (i.e., 0.0°C = 32.0° F = 273.15° K), showing that if the user enters 0.0° C, the Fahrenheit and Kelvin conversions are correct. In the case of exception conditions, we’d have to verify that the application generated appropriate exceptions in the unhappy scenarios and did not crash or print any un-handled exceptions in the happy scenarios. Then we could be reasonably certain that the application is operating correctly. While this would be possible, it certainly would be a tedious task, particularly when we realize that each time we modify the Temperature class in any way, we’d need to not only specify and run tests of the new or modified features, but also re-run all the previous tests just to make sure that nothing was inadvertently broken in the process of making the modifications. This process of re-running previous tests is called regression testing, that is, we are testing that the system did not “regress” when we are trying to make progress. When we develop code iteratively, as we’ve been doing in this text, we’d need to do regression testing early and often. Another approach to running tests is to automate them. One particular framework that has been influential in computing is JUnit, a framework for unit testing.3 Unit testing focuses on individual “units” in a program rather than on the whole program itself. In our temperature example, unit tests would focus on the temperature class itself, ignoring the user interface and any other unrelated “units” entirely. The basic idea of JUnit testing is that we can write simple test cases that “exercise” aspects of our code and alert us to any problems. JUnit uses predefined assertion methods for this purpose. For example, if we wanted to satisfy ourselves that Java is implementing addition properly, we could write a set of JUnit assertions such as: assertEquals(1+1, 2); 3 Complete information on JUnit can be found at http://www.junit.org/. 10-11 When executed as part of a full JUnit test, this individual assertion command evaluates its two arguments and reports an error if they are not equal. This assertion runs without reporting an error because 1+1 is, indeed, equal to 2. We could then add more assertions to test the range of addition functionality. We can also run the following overloaded versions of the assertion command: assertTrue(true || false); assertEquals(3.14159, Math.PI, 1e-5); The first call evaluates its argument and reports an error if the result is not true; this example does not report an error because the Boolean expression (true OR false) is, indeed, true. The second call is similar to the assertEquals() shown above except that it expects double values for its arguments; because double values may be tested with differing levels of precision, JUnit requires that this test supply a third argument specifying the required precision; this example does not report an error because the Java Math library’s pre-defined value for PI is, indeed, equal to 3.14159 to the precision of 5 decimal places (1e-5 = 10-5). For our temperature class example, we might want to write testing code that looks like this: Temperature temperature = new Temperature(0.0, 'C'); temperature.setScale('F'); assertEquals(32.0, temperature.getDegrees(), 1e-5); temperature.setScale('K'); assertEquals(273.15, temperature.getDegrees(), 1e-5); This code segment constructs a temperature object, temp, initializing it to 0○ C. It then resets that temperature object’s scale to Fahrenheit and asserts that its degrees value is 32.0 as required. The code then resets the scale to Kelvin and asserts that its degrees value is 273.15 as required. If this code runs through without any errors, we know that our Temperature class handles the first test case properly. JUnit provides a framework for specifying suites of test cases such as this, running those suites automatically and reporting any errors that it finds. For example, we could write a JUnit test class that implements this given test case as follows: import static org.junit.Assert.*; import org.junit.Test; public class TemperatureTest { @Test public void equalityTests() { Temperature t; try { t = new Temperature(0.0, 'C'); t.setScale('F'); assertEquals(32.0, t.getDegrees(), 1e-5); t.setScale('K'); assertEquals(273.15, t.getDegrees(), 1e-5); } catch (Exception e) { fail("inappropriate exception raised..."); } } } 10-12 This code implements our first test case (0.0°C = 32.0° F = 273.15° K ) as a class, TemperatureTest, containing a method, equalityTests(). However, this is no ordinary class. We can’t run TemperatureTest as a Java application because it has no main() method. Instead, we run this class as a JUnit test class. Your Java IDE will likely provide an execution option for this purpose. Furthermore, equalityTests() is no ordinary method. It is marked with the @Test annotation, which tells Java that this is a unit test method that should be run as a JUnit test case.4 JUnit runs all the test methods and reports any failed assertions that it finds. This particular test method repeats the sample test code described above, but adds the try-catch block that Java requires we add in order to handle the exception that the setScale() method might raise for invalid temperatures. If our Temperature class is coded properly, it should pass all the assertions and not throw any exceptions. However, if our Temperature class is coded improperly, we would like JUnit to report this to us so that we can fix our code. JUnit does this as follows. Java runs the try{} block through one statement at a time. As discussed above, JUnit will report an error if any of the assertion commands fail. As discussed in the previous section on exception handling, Java transfers control from the try{} block to the catch{} block if any of the try{} block code raises an exception. In the case of this example, we don’t want any exceptions to be raised because 0.0C and its conversions are perfectly valid temperature objects. Thus, our Temperature code would be wrong to raise any exception. For this reason, the catch{} block calls JUnit’s fail() method, which tells JUnit to report that the test has failed to execute properly, in this case because the Temperature class raised an exception inappropriately. The patterns for using some of JUnit’s more useful methods are as follows:5 assertTrue(booleanExpression) assertEquals(expectedValue, actualValue) assertEquals(expectedDoubleValue, actualDoubleValue, tolerance) fail(errorMessage) 4 Tests to see if booleanExpression returns true. Tests to see if expectedValue is equal to actualValue.. A special version of assertEquals designed for testing double values – it tests to see if expectedDoubleValue is equal to actualDoubleValue plusor-minus the given tolerance. Fail the unit test and report the given errorMessage. Java annotations such as this are new feature in Java 5 that we will not discuss in detail, see http://java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html for more details. 5 JUnit supports a variety of other test facilities, including set-up and tear-down methods; we don’t discuss them here. Tutorials and more details can be found at http://junit.sourceforge.net/. 10-13 The test cases that we previously identified include some scenarios in which we want our code to throw an exception. For example, we want the Temperature class to throw an exception if the user tries to construct a new Temperature object with the value -1.0° Kelvin. Testing this behavior, though a bit trickier than testing simple return values, can be done as shown in the following code. try { Temperature t = new Temperature(-1.0, 'K'); fail("should have thrown an error..."); } catch (IllegalArgumentException e) { // Do nothing here; this code should throw an exception! } This test method uses a try-catch block, as discussed in the previous section, but it uses it a bit differently.6 It tries to construct a temperature with the value -1.0° Kelvin, which we know should throw an exception because of the way we programmed the explicit-value constructor. If the Temperature class throws this exception as expected then control will jump directly to the catch block, and in this case the catch block has no statements, so JUnit concludes that everything worked properly. This will pass the unit test. If, on the other hand, the Temperature class does not throw an exception as we hope, then control will stay in the try{} block and pass directly to the fail() statement, which tells JUnit that this test has failed. Thus, this JUnit code passes the test if the constructor throws an exception and fails the test if the constructor does not throw an exception. To run a set of unit tests, we must ask our development environment to run the test class as a JUnit test case. The result is a summary of the success and/or failure of the test cases specified in the class. A fully successful test run is generally indicated by displaying the well-known “green bar”; a “red bar” indicates that at least one of the tests failed. By default, JUnit tests pass unless there is a failed assertion or an explicit call to fail(). As a consequence of this, and empty test, such as the following test, passes. @Test public void emptyTest() { // Empty tests pass by default. } A set of unit tests should be run whenever changes are made to the implementation. This helps to verify that the changes did not affect the correctness of the code. While it may require additional work to specify our test suite in this manner, it will almost certainly pay off in the long run given how much easier regression testing will be. One particularly vexing problem in software development is the situation in which changes to one part of a system cause unexpected errors in another part of that system. Unit testing the entire system whenever changes are made to any part of the system helps to smoke out these sorts of problems. Another benefit of automated testing accrues when we would like to modify our code without changing its functional behavior. This process is known as refactoring. For example, we might want to make the 6 JUnit 4 provides an annotation-based alternative to this version 3 practice; this approach uses this annotation: @Test(expected=Exception.class), and no try-catch in the test method itself. This text adopts the practice of using try-catch. For more information, see http://junit.sourceforge.net/doc/cookbook/cookbook.htm. 10-14 implementation more efficient or more understandable, without changing its functionality. In this case, refactoring should not affect the code’s ability to pass the automated unit tests, so passing the tests after refactoring gives us confidence that our modifications did not affect the functionality. We’ll see an example of this later in this chapter. 10.3.3. Revisiting the Example We can use the techniques shown in this section to automate a set of unit tests for the Temperature class as follows. @Test public void defaultConstructorTests() { // Verify that the default temperature is 0.0 Celsius. Temperature t1 = new Temperature(); assertEquals(0.0, t1.getDegrees(), 1e-5); assertEquals('C', t1.getScale()); } @Test public void explicitValueConstructorTests() { // Verify that other temperatures can be constructed and accessed. try { Temperature t1 = new Temperature(0, 'K'); assertEquals(0.0, t1.getDegrees(), 1e-5); assertEquals('K', t1.getScale()); t1 = new Temperature(98.6, 'f'); assertEquals(98.6, t1.getDegrees(), 1e-5); assertEquals('F', t1.getScale()); } catch (Exception e) { fail("inappropriate exception raised..."); } } @Test public void badExplicitValueConstructorTests() { // Try some invalid temperatures. badExplicitValueUtility(10.0, 'X'); badExplicitValueUtility(-0.01, 'K'); } // This utility tries to construct invalid temperatures using try-catch private void badExplicitValueUtility(double degrees, char scale) { Temperature t; try { t = new Temperature(degrees, scale); fail("Exception should have been thrown on: " + degrees + " " + scale); } catch (Exception e) { // Do nothing; this code should throw an exception. } } These unit tests test the default and explicit-value constructors. Note that they test both happy and unhappy scenarios. For example, the simplest happy scenario is the default constructor, which should 10-15 construct a new temperature representing 0.0° Celsius. Other happy scenarios tested include 0.0° Kelvin and 98.6° Fahrenheit. Unhappy scenarios may occur as well, so they must also be tested. The badExplicitValueConstructorTest() unit test tries to create two invalid temperatures: 10.0° X (an invalid scale) and -1.0° Kelvin (below absolute zero). Both of these tests are handled by badExplicitValueUtility(), and utility method that handles the details of running code that is supposed to throw errors. @Test public void badSetScaleTest() { Temperature t = new Temperature(); try { t.setScale(' '); fail("Exception should have been thrown..."); } catch (Exception e) { // Do nothing; it should throw this exception. } } This test uses code that is similar to that used in badExplicitValueUtility(), but it is designed to test the setScale() mutator instead. Clearly the space character is not a valid scale designator and this call should, therefore, throw an error. @Test public void equalityTests() { Temperature t; try { // Test freezing temperature equivalents. t = new Temperature(0.0, 'C'); t.setScale('F'); assertEquals(32.0, t.getDegrees(), 1e-5); t.setScale('K'); assertEquals(273.15, t.getDegrees(), 1e-5); // Test boiling temperature equivalents. t = new Temperature(373.15, 'K'); t.setScale('F'); assertEquals(212.0, t.getDegrees(), 1e-5); t.setScale('C'); assertEquals(100.0, t.getDegrees(), 1e-5); // Test absolute zero temperature equivalents. t = new Temperature(-459.67, 'F'); t.setScale('K'); assertEquals(0.0, t.getDegrees(), 1e-5); t.setScale('C'); assertEquals(-273.15, t.getDegrees(), 1e-5); // Test large temperature equivalents. t = new Temperature(1799540.33, 'F'); t.setScale('C'); assertEquals(999726.85, t.getDegrees(), 1e-5); t.setScale('K'); assertEquals(1000000.0, t.getDegrees(), 1e-5); } catch (Exception e) { fail("inappropriate exception raised..."); } } 10-16 @Test public void comparatorsTest() { Temperature t1; try { t1 = new Temperature(5.0, 'K'); assertTrue(t1.equals(t1)); assertFalse(t1.lessThan(t1)); assertTrue(t1.lessThanOrEqualTo(t1)); assertFalse(t1.greaterThan(t1)); assertTrue(t1.greaterThanOrEqualTo(t1)); Temperature t2 = new Temperature(25.0, 'C'); assertFalse(t1.equals(t2)); assertTrue(t1.lessThan(t2)); assertFalse(t1.greaterThan(t2)); assertTrue(t1.lessThanOrEqualTo(t2)); assertFalse(t1.greaterThanOrEqualTo(t2)); Temperature t3 = new Temperature(5.0, 'F'); assertFalse(t1.equals(t3)); assertTrue(t1.lessThan(t3)); assertTrue(t1.lessThanOrEqualTo(t3)); assertFalse(t1.greaterThan(t2)); assertFalse(t1.greaterThanOrEqualTo(t3)); } catch (Exception e) { fail("inappropriate exception raised..."); } } @Test public void changeTests() { try { Temperature t1 = new Temperature(50.0, 'F'); t1.change(10.0); assertEquals(60.0, t1.getDegrees(), 0e-5); t1.change(-20.0); assertEquals(40.0, t1.getDegrees(), 0e-5); } catch (Exception e) { fail("inappropriate exception raised..."); } } public void testCopy() { Temperature t1; try { t1 = new Temperature(10000.5, 'C'); Temperature t2 = t1.copy(); assertTrue(t1.equals(t2)); assertNotSame(t1, t2); } catch (Exception e) { fail("inappropriate exception raised..."); } } 10-17 These tests all exercise happy scenarios, but they must, nevertheless, wrap their code in try-catch blocks because the methods they call could potentially throw errors. The compiler requires that the try-catch blocks be there even if we happen to believe that the execution should not throw errors. The unit tests help to verify that this is, in fact, the case. 10.4. User Testing The previous section discussed approaches for testing program correctness. Correctness is a necessary but not sufficient criterion for robust, high-quality software. The usability of the system, that is, the measure of how easy it is to learn for an end user, is also important. A correct program that is not usable by its intended users has little more value than an incorrect program. It is, therefore, important to test both correctness and usability. 10.4.1. Testing Usability Usability cannot easily be tested using JUnit because JUnit tests don’t have easy access to the interaction between the user and the system. This is true both of the console-based applications we have seen so far as well as the graphical user interfaces we’ll see in later chapters. More importantly, JUnit tests cannot anticipate the sorts of questions that users are likely to have when encountering the GUI for the first time. This kind of testing can only be done effectively by asking representative users to actually use the interface. Note that while developers should certainly try out the GUI prototypes to make sure that they run as expected, we cannot expect a developer to find the sort of usability problems common to today’s interactive systems. Developers know too much about GUIs and far too much about the system being developed to ever have a good idea of what it would be like to encounter the interface for the first time. Thus, we must perform our usability testing with the help of more representative users who have not seen the program before. The design and execution of systematic usability tests is based on the principles and concepts of experimental psychology and is, thus, beyond the scope of this text. As a simple rule of thumb, however, it would be a good idea to ask four to six users to try out your system. Let these test users be representative of your real users. If your system is a math quiz for high school students, then find some high school math students; if it’s a layout design tool for art students, then find some art students. You can expect these test users to see your system as anyone else would see it, and that is potentially valuable to you as a system developer. Our working definition of software quality throughout this text has been that our applications should be correct, efficient, understandable to other programmers and usable to its prospective users. It’s not good enough to produce applications that satisfy only the first three criteria. We want more; our potential users deserve more; they deserve an application that doesn’t just work, but is a joy to use. 10-18 10.4.2. Separating the Model from the View The distinction between functional unit testing and usability testing has implications for the class design of our systems. To support these two types of tests, we would like, as much as possible, to design our class structure to separate what are commonly called the model and the view: Model – The model comprises the core functionality of the class itself. In the temperature example, the model is the implementation of the temperature class itself, with its implementations of the attributes and behaviors of the temperature objects. View – The view comprises the use, or uses, of the class in context. In the temperature example, the views include the use of the temperature class in the TemperatureConsole application and in the TemperatureTest test cases. The class structure that we desire can be depicted as follows, where the views are on the left and the model on the right. TemperatureConsole Temperature TemperatureTest Separating these concerns is generally a good object-oriented design practice. The temperature class should only concern itself with temperature-function-related elements. It shouldn’t care how it is used. That’s the job of the view classes, which may use temperature objects in a console-based application, as TemperatureConsole does, or in unit tests, as TemperatureTest does. This practice generally makes it easier to design and implement the classes. One additional benefit of this design is that it allows us to use automated unit testing on all the functionality provided by the model. As discussed above, we can’t easily unit test user interfaces, so encapsulating all non-interface-related functions in the model allows us to unit test those functions easily. We then only have to do manual user testing on the user interface. 10.4.3. Revisiting the example In this section we consider the usability of a temperature conversion console application. The user experience envisaged in Section 10.1 was rather simple, allowing the user to enter a temperature and a target scale, as follows: Enter a temperature: 40 f Convert to: c Result: 4.444444444444445 C 10-19 The system output is shown here in black and the user input is shown in green. This is not the most friendly of interfaces given that users are unlikely to know how to enter a temperature value. It also turns out that the current implementation doesn’t handle unhappy scenarios properly, as seen here: Enter a temperature: forty degrees fahrenheit Exception in thread "main" java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:909) at java.util.Scanner.next(Scanner.java:1530) at java.util.Scanner.nextDouble(Scanner.java:2456) at c10quality.text.temperature.TemperatureConverterConsole.main(TemperatureConver terConsole.java:21) Here, the user had entered their temperature using an unsupported format (i.e., “forty” rather than “40”) and has received an unhandled exception in response. This is not a usable interface in its current form. Asking real users to try the system out in a usability test would almost certainly identify these problems. While there is only so much we can do to make a console application like this one more usable, we can certainly give the users some idea of what the input should look like and handle the exceptions more gracefully. Here is a console application that does that: /** * TemperatureConverterConsole implements a simple text-based interface for the * temperature converter application. * * @author kvlinden * @version Fall, 2009 */ public class TemperatureConverterConsole { public static void main(String[] args) { System.out.print("Enter a temperature (e.g., 98.6 F): "); // System.out.print("Enter a temperature: "); Scanner keyboard = new Scanner(System.in); try { double degrees = keyboard.nextDouble(); char scale = keyboard.next().charAt(0); Temperature temperature = new Temperature(degrees, scale); System.out.print("Convert to: "); temperature.setScale(keyboard.next().charAt(0)); System.out.println("Result: " + temperature); } catch (InputMismatchException e) { System.out.println("Please use a valid format."); } catch (Exception e) { System.out.println(e.getMessage()); } } } This code gives the user an example of the temperature input and helps them through the following happy scenario: 10-20 Enter a temperature (e.g., 98.6 F): 40 f Convert to: c Result: 4.444444444444445 C This code also handles some of the unhappy scenarios a bit more gracefully: Enter a temperature (e.g., 98.6 F): forty degrees fahrenheit Please use a valid format. and Enter a temperature (e.g., 98.6 F): -1.0 K Invalid Temperature: -1.0 K We’ll use graphical techniques to develop more useable interfaces when we get to Chapter 12. 10.5. Enumerated Types As a final enhancement to our temperature program, we consider the understandability of the temperature class. Where usability measures how easy an application is to learn for its end users, understandability measures how easy the units of the system are to learn for other programmers. As an example, consider the representation of the scale of a temperature object. Currently, we represent the scale using a character value (e.g., 'F', 'C' or 'K') and we must admit that this is somewhat clunky. Programmers using our temperature class in their applications are required to remember and abide by these codes. The Java compiler helps a bit. It only accepts characters for the explicit-value constructor and the setScale() mutator, and thus an invocation like new Temperature(98.6, "Fahrenheit") would be flagged as a compiler error. "Fahrenheit" is not a character. However, there are other errors that the compiler doesn’t catch. For example, the invocation new Temperature(98.6, 'X') is syntactically correct. Currently, the temperature class throws exceptions in these cases, but it would be nice if there were some way to specify that there are exactly three scales and provide some convenient way of enumerating them. This would not improve the correctness of the system, but it would help to improve its understandability to other programmers and simplify the implementation. Fortunately, Java provides a mechanism that allows us to enumerate a finite list of field values and treat that list as a new type. This mechanism is called enumerated types. 10.5.1. Working with Enumerated Types We can define an enumerated type as shown here. This example implements a finite list of currency types that we could profitably use in a currency conversion application. enum Currency {US$, CANADIAN$, AUSTRALIAN$}; This statement declares a new type called Currency whose legal values are restricted to only those fields listed in the declaration: US$, CANADIAN$, AUSTRALIAN$. The fields can be any legal Java identifier (recall that identifiers can contain the $ character). This declaration allows us to work with variables of this new type. 10-21 Currency aCurrency = Currency.US$; Here, Java expects us to use the qualified version of the Currency value (Currency.US$) rather than just the unqualified version (US$). Because aCurrency is declared to be of type Currency, we cannot use any other values than those listed in the Currency definition. The following code segment prints out a message based on the value of aCurrency.7 if (aCurrency == Currency.US$) { System.out.println("it's US dollars..."); } else if (aCurrency == Currency.CANADIAN$) { System.out.println("it's Canadian dollars..."); } else { System.out.println("it's Australian dollars..."); } Note that in this code, we don’t need to provide fourth option because we know that the compiler will prevent the programmer from ever using any Currency value not listed in the Currency declaration. The key advantage of using enumerated types is that it allows us to tell Java about this new type and engage the Java compiler as a partner in ensuring the robustness of our application. We no longer have to explicitly program exception handling code to handle the situations in which programmers use the wrong terms; the Java compiler now does this for us. For example, if we forget how to spell “Australian”, we can rely on the compiler to enforce the use of the correct term (Currency.AUSTRALIAN$). In addition, using an incorrect name leads to a compiler error that we can fix quickly and easily rather than to an exception thrown at runtime that takes longer to discover and to fix. 10.5.2. Revisiting the Example In this iteration, we refactor the Temperature class to use an enumerated type to represent the legal list of temperature scales. public class Temperature { public static enum Scale { FAHRENHEIT, CELSIUS, KELVIN } private double myDegrees; private Scale myScale; // The other declarations are unchanged. public Temperature() { myDegrees = 0; myScale = Scale.CELSIUS; } 7 Because the new enumerated type is integer-compatible, we can more effectively use a switch statement rather than a multi-branch if statement to distinguish its values. 10-22 public Temperature(double degrees, Scale scale) throws Exception { if (isValidTemperature(degrees, scale)) { myDegrees = degrees; myScale = scale; } else { throw new Exception("Invalid Temperature: " + degrees + " " + scale); } } // The getDegrees() accessor is unchanged. public Scale getScale() { return myScale; } // The comparison and copy methods unchanged. public void setScale(Scale scale) throws Exception { if (scale == Scale.CELSIUS) { convertToCelsius(); } else if (scale == Scale.FAHRENHEIT) { convertToFahrenheit(); } else { convertToKelvin(); } } private void convertToCelsius() { if (myScale == Scale.FAHRENHEIT) { myDegrees = 5.0 / 9.0 * (myDegrees - 32.0); } else if (myScale == Scale.KELVIN) { myDegrees = myDegrees - 273.15; } myScale = Scale.CELSIUS; } // The other conversion methods are unchanged. public static boolean isValidTemperature(double degrees, Scale scale) { if (scale == Scale.CELSIUS) { return degrees >= ABSOLUTE_ZERO_CELSIUS; } else if (scale == Scale.FAHRENHEIT) { return degrees >= ABSOLUTE_ZERO_FAHRENHEIT; } else { return degrees >= ABSOLUTE_ZERO_KELVIN; } } public String toString() { return myDegrees + " " + myScale; } } This new version of the Temperature class declares a new type, Scale, that enumerates the three supported temperature scales. We choose to declare this type as public and static so that it is accessible to 10-23 other classes and is shared by all temperature objects respectively. We’ve also replaced the char type of all those parameters and variables that represented scales with Scale. This new Temperature class requires changes to the controller class as well. import static c10quality.text.temperatureEnumerations.Temperature.Scale; import java.util.InputMismatchException; import java.util.Scanner; /** * TemperatureConverterConsole implements a simple text-based interface * for the temperature converter application. * * @author kvlinden * @version Fall, 2012 */ public class TemperatureConverterConsole { public static void main(String[] args) { System.out.print("Enter a temperature (e.g., 98.6 F): "); Scanner scanner = new Scanner(System.in); try { double degrees = scanner.nextDouble(); Temperature.Scale scale = readScale(scanner); Temperature temperature = new Temperature(degrees, scale); System.out.print("Convert to: "); temperature.setScale(readScale(scanner)); System.out.println("Result: " + temperature); } catch (InputMismatchException e) { System.out.println("Please use a valid format."); } catch (Exception e) { System.out.println(e.getMessage()); } } // Read char scale from the keyboard and convert to // Temperature.Scale. public static Scale readScale(Scanner scanner) throws Exception { char scaleInput = scanner.next().charAt(0); if (Character.toUpperCase(scaleInput) == 'C') { return Scale.CELSIUS; } else if (Character.toUpperCase(scaleInput) == 'F') { return Scale.FAHRENHEIT; } else if (Character.toUpperCase(scaleInput) == 'K') { return Scale.KELVIN; } else { throw new Exception("Invalid scale: " + scaleInput); } } } This controller must configure its temperature object using, in part, the enumerate scale names, so it imports those names (see the very first import static line) and uses them when it must read a new scale from the user. The process of converting from character input to Temperature.Scale is performed by the readScale() method. This method is a bit messy, but moving the input and 10-24 conversion to Temperature.Scale to the console interface greatly simplifies the Temperature code and makes it less dependent on the implementation of the user interface. This modified version of the Temperature class should behave in the same manner as the original version. That is, the upgrades only should affect its efficiency and understandability, but not its correctness. Thus, the code should pass the automated unit tests without any modification to the test cases themselves. If it does, we have confidence that we’ve done the refactoring correctly (see the discussion of refactoring earlier in the chapter). 10.6. Example Revisited Now that we have a Temperature class in which we have some confidence of correctness and understandability, we consider the application’s usability. Having designed the system through several iterations, we have a good idea of how it works and would likely find it hard to imagine that anyone would have problems figuring out how to use it. Nevertheless, we should ask potential users to try the application out and closely observe how they fare with the features and user interface. We will not run such a study for this text, but there are a number of things that we’d like to determine if we did, including: Will the users understand that they must enter numeric values (e.g., “40”) rather than textual ones (e.g., “ 40”, with leading spaces, or “forty”) for the degree magnitudes? We’ve provided a simple input example and have programmed the application to catch this error and respond with a simple text message, but it’s unclear whether this will be enough to make the system’s use seem easy to users. Will the users know that the three legal scales are coded by F, C and K? Given that the interface provides no information on this, it’s likely that they’ll see the Fahrenheit scale in the example and guess the Celsius scale, but what about the Kelvin scale? Will the users immediately understand and appreciate the decidedly textual mode of the system output, or would they respond more favorably to a graphical, animated form of output? Because we haven’t done such a study, we can’t provide definitive answers to these questions. As programmers, we know this interface very well, from both the internal and external perspectives, and we are, therefore, unlikely to guess how someone else would experience our program. This fact about the nature of developers makes user testing critical to the development of usable applications. 10-25