Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Course: CMPT 101/104 E.100 Thursday, October 26 Lecture Overview: Week 8 Announcements • Expectations Errors, Debugging, Exceptions • Unit Tests • Test Cases • Batch files, Shell scripts • Program Traces • Exceptions • Assering Conditions For Next Class… Assignment 4 is due Wednesday, November 8 • Review Chapter 8 • Read ahead: Chapter 9 • Midterm 2: next Thursday at 5:30PM. Note from last week public static void main(String args[]) { int test = 0; if (test){ int test = 1; } } Will this code compile? Answer: No. The principal known as shadowing does not apply here. You can declare an instance variable and a local variable with the same name, but you cannot declare two variables with the same name in the same block of the same method. On the other hand, this will compile and run. (Why?) public static void main(String args[]) { if (true) { int count = 1; } else { int count = 0; } } Note: Correction to Week7 notes: Do nothing. Variables are always initialized in objects by default: numerics are set to 0, Strings are set to the empty string, null for object references, false for booleans… Strings are also set to null by default. Typical Software Projects A typical program can easily be made up of thousands or lines of code. It is likely that a given program will be 10's of thousands or even hundreds of thousands of lines of code. Smaller programs exist, though they might be called Utilities when they are only hundreds of lines of code long. Operating Systems are exceptionally large programs. Windows 98 has O(11,000,000) lines? At a .1% error rate you have approximately 11,000 lines of code that contain errors, and this is ten times better than industry standard, if Microsoft should be accurate in their assertions. You can probably see that testing becomes an important issue. (In fact, QA is a growth field in the world of software. Lots of jobs, and more coming.) Divide and Conquer You can no doubt guess that a program cannot be written in one big effort. Rather, the task is broken down into manageable bits: packages, classes, and methods, a method being (hopefully) 1 – 100 lines long, with the sweet-spot being around 40 lines of code. The process of breaking down tasks in this manner is often referred to as Divide and Conquer. The Unit Test is a test applied to a single method or a set of cooperating methods. Unit tests don't accomplish anything useful other than to give some indication that your code works properly. Consider Test.java that is included with Assignment4: import ParseString; class Test{ public static void main(String args[]) { final String DELIMITERS=",;\t"; String one = "HERE;ARE;4;TOKENS"; ParseString parser = new ParseString(one,DELIMITERS); System.out.println("String one: \n\n"); while(parser.hasMoreData()) { String next = parser.getData(); System.out.println(next); } System.out.println("End of Program.\n\n"); } // end main } // end class If the ParseData class is properly created the String "HERE;ARE;4;TOKENS"; should be printed out in the following manner: HERE ARE 4 TOKENS End of Program. This is not an exhaustive test. Only 1 single delimiter type has been tested. Unusual error conditions have not been tested. Nevertheless, if Test runs according to expectation you know that ParseString works reasonably well. Given that a program may have thousands of such classes, you better be testing them along the way. If you don't test until the program is about to be released to market you can pretty well bet that all hell is going to break loose. Example 2: SqrtTest.java (p315) import Numeric; class MathAlgs { // This method finds the square root of input, in theory public static double sqrt(double input) { if (input <= 0) { return 0; } double next = input; double current = 0; boolean approx_equal = false; do { current = next; next = (current + input/current)/2; approx_equal = Numeric.approxEqual(next, current); } while (!approx_equal); return next; } } Suppose that we have compiled the above method. Does it work properly? Does it work properly in all cases or in some cases? Test Harness: SqrtTest1.java // This test harness allows us to enter test data interactively import ConsoleReader; import MatchAlgs; class SqrtTest1 { public static void main(String[] args){ ConsoleReader console = new ConsoleReader(System.in); boolean done = false; while (!done) { String inputline = console.readLine(); if (inputline==null) { done = true; } else { double x = Double.parseDouble(inputline); double y = MathAlgs.sqrt(x); System.out.println("square root of " + x + " = " + y); } // end else } // end while } // end main } // end class Entering data interactively is good for trivial tests, but it isn't much fun to continually repeat. Redirection from file would help, though only a little. We would still have to manually check the results. Test Harness: SqrtTest2.java import ConsoleReader; import MathAlgs; class SqrtTest2 { public static void main(String[] args){ for (double x = 0; x <=10; x+=0.5) { double y = MathAlgs.sqrt(x); System.out.println("square root of " + x + " = " + y); } } // end main } // end class This approach has an advantage: it tests boundry values, a very important area of input where errors often occur. Still, 0.5 increments is hardly an exhaustive test. We would prefer to do a little more work to be sure that we had a pretty accurate set of results. The best situation would be to have the Test Harness (program) test the results of the method itself. Test Harness: SqrtTest3.java // Have the Test Harness look for errors and report them import import import import ConsoleReader; MathAlgs; Numeric; java.util.Random; class SqrtTest3 { public static void main(String[] args){ Random generator = new Random(); for (int count=0; count<10000; count++) { double x = 1.0E6*generator.nextDouble(); double root = Math.sqrt(x); double test = MathAlgs.sqrt(x); if (Numeric.approxEqual(root,test)==false) { System.out.println("\n\n\nError! generated number: " + x); System.out.println("Actual Root: "+root); System.out.println("MathAlgs approximation: "+root); } } } // end main } // end class In this case we have the Test Harness test the method against a large sample set of random values. The program only bothers us if something unexpected does occur. Note: even we change the sqrt() method of MathAlgs in the future we can simply run this test again without having to change a thing. This is often called regression testing. C8.2 Selecting Test Cases How should you test? What should you test? How much testing is enough testing? A: You often do not need to worry about these questions since you are constrained by deadlines and money more often than not. Should you have the luxury of doing things more rigorously, here are some things to consider doing: Positive Tests This involves testing typical data: the kind that occurs 80+% of the time. Boundry Tests This involves data that is still valid but at the extremes. For example, 0 is a valid input for a sqrt() method though not common and certainly at the extreme of the range of possible values. Negative Tests This involves using invalid and / or unexpected data. –5, -Five, "", and null are invalid input values for a sqrt() method, but they can easily occur. Seperating Error Checking It is always easy to write code that does not have to deal with invalid data or strange circumstances. It may be worth your while to write code like this and separate the issue of error-testing by creating special objects or methods that do this for you. // Example: Typical Case String input = console.readLine(); ProcessData(input); // ProcessData has consider all the possible error conditions // in the input. // Alternatively String input = console.readLine(); if (validData(input)) ProcessData(input); else Error_msg(input); Rationale: Code logic can become very complex when there are several possibilities that you must be concerned about. Writing the code is only a part of the work. Maintaining code is often more important than the original creation. In the above example you can see that a change in requirements does not affect a lot of code. For example, if you have to change the valid types of input data in the future, the second option only requires you to change the validData() method. The method ProcessData() in first option might have complex program logic in order to deal with all the potential error conditions. Minor changes in the code could easily introduce errors that may or may not surface immediately. At a minimum you can see that the second option divides a single method into three separate methods, each with a straightforward purpose. This is also highly desirable and easy to understand and maintain. Black Box Testing These types of tests start with the assumption that you know nothing about the implemation of the method of class. You just test to see whether the method or class performs according to its specifications. This sort of testing does not generally cover every possible circumstance, therefore; it only shows that some types of errors do not exist. Under these circumstances it is possible for latent bugs to exist. These are errors that do not show up, possibly for years, until some unusual circumstance arises. In general latent bugs are only an annoyance. Occasionally they kill someone. White Box Testing This type of testing is very exhaustive (and exhausting) but can insure that a given method of class is sound. This is accomplished by testing every possible circumstance: every possible input; every possible path of execution; and so forth. Test Coverage is a term which is used to describe how much of the given possible paths of execution and data your test cases actually cover. There are some automated tools (programs) on the market designed to do this job. Often your programs will be too complex to do white box testing without such a tool. Regression Testing Often an attempt to fix one bug will create another one or cause old bugs to reappear (called cycling). Due to this possibility regression testing, or the testing of a program against past errors is very important. Test.java is a good example of the type of program that can be used for regression testing. Strategies In the ideal world you would envision the end from the beginning (as per Stephen R. Covey). In other words, you would know the kind of classes and methods that you need to write before you even start. Often, however; you will be working on new projects where you don't know what you want when you start out. You have to feel your way along. In cases like these you will save yourself a lot of time by writing out some test cases before you start writing any code. For example, here is the sample input from Assignment4 - input.txt: 1,126,Bill Tait,890.00,125 2,1250.00,133 3,5250.00,220 3,250.00,220 4,591 5,640 6,230 1,145,Final Account,400.00,140 7 Without these clues Assignment4 would probably be twice as hard (or harder) to create! Yet, in the real world (ie: not school) your boss is often going to give you assignments with no sample data to start with. If you begin writing code without asking a lot of questions and creating helps like Test Cases, your life is going to be not very fun. Test Cases: do them or suffer. Productivity Note: Batch Files Many operations you do will be repetitive. A simple strategy for reducing ugly work is to create batch files with your commonly done tasks. For example: javac someClass.java java someClass <input1.txt >output1.txt java someClass <input2.txt >output2.txt java someClass <input3.txt >output3.txt These may be commands that you run repetively as you write, test, and debug someClass. Alternatively, you could create a batch file such as A4.bat with these lines in it, then type A4 everytime you make a minor change to someClass and save it. It could save you a whole lot of work. Batch files even accept parameters: test.bat java %1 <input1.txt >output1.txt java %1 <input2.txt >output2.txt java %1 <input3.txt >output3.txt When you type test someClass the string someClass becomes the parameter denoted by %1. If might prove handy sometime, particulary when you are running multiple versions… See p. 320 – 321 for more details. C 8.4 Program Traces These are pretty common when you don't have a debugger. Here's a good example: import Numeric; class MathAlgs { public static double sqrt(double input) { if (input <= 0) { return 0; } double next = input; double current = 0; boolean approx_equal = false; do { current = next; next = (current + input/current)/2; approx_equal = Numeric.approxEqual(next, current); //System.out.println(next); } while (!approx_equal); return next; } } The only thing important here is the line that has been commented out. There are times when you need to know what is going on in your program and the only way to tell is creating a trace. In most circumstances you would be using an IDE (Integrated Development Environment) which would allow you to walk through the code watching the execution one line at a time. You will probably have to wait for 201… C 13.3 Exceptions (p 322) In the real world things go wrong, often. When writing code you spent 20% or less of your time dealing with that which will occur 90% of the time or more. Then you spend most of your time dealing with unusual errors or conditions. Why not just ignore them if they are so rare? Answer: programs are just like cars, planes, VCRs, Stereos, and so forth. They are just a product, and the public likes it when their products work, and they want them to work all the time. Answer2: Sometimes the repercussions are severe. Traditionally programs have handled error conditions by explicitly checking for them at every step of the way. For example: public static double sqrt(double input) { if (input <= 0) { return 0; } //etc… Here we see that sqrt() explicitly checks for valid input before proceeding. This is generally a good thing, however; this process can be quite ugly. Let's consider a case where the required operation is quite laborious and complex. Scenario: Database Work // retrieve the next record status = retrieve_next_record(); // test for errors if (status = NO_RECORDS) { continue; } else if (status = DB_FAILURE) { record_error(); return DB_FAILURE; { // find match based on ID status = match_by_userID(); // test for errors if (status = DB_MATCH) { update_record(); status = update_database(); // test for errors if (status = DB_FAILURE) { record_error(); return DB_FAILURE; { else { continue; } } else if (status = DB_FAILURE) { record_error(); return DB_FAILURE; { else if (status = DB_NO_MATCH) { // search on different field // etc. //etc… } // less than 40 lines of reasonabley legible code So Ugly… Diligently checking the success of every operation that can fail is the only responsible course of action, yet… you can see how tedious and messy the whole job can be, even from this trivial example. The text calls this 'programming for failure', and that is about the size of it. There may be so many different types of errors to check for that the programmer could miss those errors, or simply be lazy. Yuck. Our motivation is to find some way of managing all of the potential errors in a fashion that is elegant and as painless as possible. Note: this is a prime case where using classes can generate some extra work in the short term but save a whole bunch of time in the long run. Certain types of errors are very common. Why keep writing the same code over and over again? Enter exceptions… The Exception Class Java has a class designed to deal with error conditions called the Exception class. Exceptions share some common properties and there are several types of exceptions that deal with fairly common problems. For example: • EOF Exception (This occurs when you try reading from a file and the file is already empty) • FileNotFound Exception (This occurs when you try to access a file that does not exist) • NullPointer Exception This is what happens when you try to reference something that doesn't exist- the reference is set to null. Example: BankAccount account1 = new BankAccount(); /// etc… account1 = null; account1.deposit(50.00, myUID); // wrong • NumberFormat Exception (This happens when you try to read that double as an integer, and so forth) Example: String input = console.readLine(); int oops = Integer.parseInt(input); // !! Many other types and subtypes of Exceptions exist. Once again, the Exception is a class: a type of entity. How to use Exceptions The procedure is straightforward: if an unusual condition occurs you make (instantiate) an Exception and throw it. You may chose to deal with an Exception anywhere you want. This is called catching an Exception. Here is an example. // suppose we are trying to read in 100 lines try { for (int ii = 1; ii <= 100; ii++) { input = console.readLine(); if (input == null) { // !! not enough lines read yet! String msg = "File only contains " + ii + " lines!!!"; EOFException exception = new EOFException(msg); throw exception; } else { process_input(input); } } catch (EOFException e) { String msg = "ERROR PROCESSING FILE: "; System.out.println(msg + e); } So, what is it good for? I admit that this does not immediately look so wonderful, however; let us consider a more realistic bit of code: try { // hundreds, and hundreds, and maaaabe even // thousands of lines of gritty, // painful, nasty code. } catch(some_Exception_type e) { deal_with_the_problem(); } The point here is that you can deal with 10's of similar error conditions in the same way and in one place. What's more, the above code and it's logic just became a whole lot easier because you didn't have to deal with every error condition at every moment. You just created an Exception and threw it, then got on with business. You can also deal with different Exceptions individually: try { // … various stuff… } catch (EOFException e) { handle_EOF_errors(e); } catch (NumberFormatException e) { handle_NumberFormat_errors(e); } // etc. The point: you can be as general or as specific as you want when handling your errors. If you want to catch every type of exception you can use this line of code. catch(Exception e) Exceptions: Powerful We will come back to Exceptions next week. For now you should understand that they are a powerful aspect of Java. You don't know many of the Exception classes yet, but that will come. Assertions When creating complex classes and methods you can't be sure that things work as you would like them to. How can you check to be sure that your methods work as they should? Option 1: Trace Statements System.out.println("The value of the data is " + data); You could put these trace statements throughout your code and watch as your program runs. Pros: • Any errors that occur will be obvious • Very simple to implement Cons: • real ugly • Lots of work • Non-permanent: Sooner or later you will have to remove those trace statements, or at least comment them out. Option 2: Assertions You could create a class that only had one job: to test a given condition and display some error information if that condition were false. Here is one possible implementation of an assertion class. public class Assertion { public static void check(boolean condition) { if (condition <> true) { System.out.println("Assertion failed"); // construct a special type of Exception that // shows us where the problem occurred. Throwable stack_trace = new Throwable(); stack_trace.printStackTrace(); // Exit the program System.exit(1); } // end if } // end method } // end class Here is an example of how you might use this class public static double square_root(double input) { Assertion.check(input >= 0); // etc… } Since we don't want to attempt to take the square root of a negative number the above test seems reasonable. If the input is < 0 the assertion object will terminate the program. The printStackTrace() method will show all the pending methods. A typical stack trace might look something like this: java.lang.Exception at Assertion.check(Assertion.java: 10) at Math_functions.square_root(Math_functions: 3); at Test.main(Test.java: 45) Assertions: How does this implementation measure up? Cons: ? Pros (of this implementation) • Well structured: you can decide how you want to handle errors in program logic and implement the (possibly complex) solutions. Trace statements are pretty unprofessional. • Allows you to differentiate between errors in logic versus errors that are beyond your control (ie, the user enters some bad data versus your code is just wrong) • Easy to maintain: you can change the way that errors are handled in one place, rather than in many places in your program. • Minimal impact on performance: only 1 statement needs to be executed for the test • Can be left in the code: you don't have to delete or comment them out when releasing a production version of the software.