Download Course: CMPT 101/104 E.100 Lecture Overview: Week 8

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
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.