Web Application Testing in Ruby: Web Application Testing in Ruby: Catch and Throw
In my opinion, objects are all about data. In programs using objects to simulate real world things like cannonballs, such data might be position, velocity and mass. In business programs, an object might contain a person's first and last name, employee number, job classification and health insurance.
An object is a wonderful place to store a program's configuration information. All such info is kept in one place such that only a single object is kept global or passed in and out of subroutines.
All of these ideas precede object orientation. Since the dawn of time programmers have put all data for an entity in a data structure, and then manipulated the structure. Here's some code I wrote in 1986 to manipulate the page of a dot matrix printer. Keep in mind that back in those days, computers didn't have enough RAM for everyone to store their printed page in an 80x66 array. Much of my job back then was programming computers to print out medical insurance forms, each with about 40 boxes to fill out in very tight quarters. There were several different form layouts, and they changed frequently. So here's some 1986 C code (note the original K&R style -- no prototypes):
/* THE REPORT VARIABLE */
typedef struct
{
FILE *fil; /* report file file variable */
int y; /* y coord on page, changed only by atyxpr */
int x; /* x coord on page, changed only by atyxpr */
int pglength; /* lines per page, changed only by openrpt */
int stringlength; /* maximum length of string to be printed */
int lineno; /* line number, changed only by applcatn pgmr */
int pageno; /* page number, changed only by applictn pgmr */
char status[10]; /* set to @REPORT or @CLOSED */
} REPORT;
void atyxpr(rpt,y,x,st)
REPORT *rpt; /* the report variable pointer */
int y; /* the present vertical print position */
int x; /* the present horizontal print position */
char *st; /* the string to be printed */
{
int i;
checkopen(rpt);
if ((x == 0) && (y == 0))
{ /* continue printing at last position */
y = rpt->y;
x = rpt->x;
}
/* formfeed if the print line you're seeking is higher than the last time */
if (y < rpt->y)
formfeed(rpt);
/* insert a '^' if you've overwritten a column */
if ((y == rpt->y) && (x < rpt->x))
{
strcpy(st, st +(1 + rpt->x - x));
writestring(rpt, "^");
x = rpt->x;
fprintf(stderr, "?-warning-atyxpr- column overwrite in line %d.\n", rpt->y);
}
/* bring the print position to the new coordinates */
while (y > rpt->y)
{
linefeed(rpt->fil);
rpt->y = rpt->y + 1;
rpt->x = 1;
}
while (x > rpt->x)
{
spaceout(rpt->fil);
rpt->x = rpt->x + 1;
}
/* do the actual write of the string */
writestring(rpt, st);
/* bring the x position up to date after the write */
rpt->x = rpt->x + strlen(st);
} The REPORT structure kept track of the current position of the print head (y and x), the number of lines on a page (pglength), and the file to which to write the output (the file was usually a printer device). All this information remained persistent in the report structure.
The report structure was manipulated by a function called atyxpr(),. To print a string at a specific line and column, the programmer specified the string to print and the y and x coordinates (row and column) at which to start printing the string. Also specified was the report structure.
If the row and column were specified as both being 0, atyxpr() printed the string at the current print head position, as if the print was done by a simple printf().
If the row was the same as the current printhead row but the column was farther out, atyxpr() printed spaces until the printer head was in the desired place, and then the string was printed.
If the desired row was below the current printhead position, atyxpr() printed linefeeds to get to the desired row, printed spaces to get to the desired column, and then printed the string.
If the desired row was above the current printhead position, that meant that it needed to be printed on the next page, so a formfeed was issued, then enough linefeeds to get to the desired row, then enough spaces to get to the desired column, and then the string was printed.
What does this have to do with Ruby? Believe it or not, there's a purpose to showing this obsolete C code from an era of monospace printers and computers too anemic to store 80x66 worth of characters. That purpose is to show that there's absolutely nothing new about congregating all data about a specific entity or device in a single place, nor is there anything new about encapsulation. You do not need object orientation to do these things. I did it in 1986 using K&R C, and people were doing it long before me.
What IS new about object oriented programming (OOP) is that you can store the subroutines that manipulate the data (atyxpr() in this example) right along with the data. But so what? What's the advantage?
The advantage is something called namespace collision. The name of the subroutine manipulating the data is in scope only within the context of that data. If that name is used elsewhere, it refers to a different subroutine. In old C, if you had geometric figures square, circle, point and parabola, look what you'd need:
• circle_move(circleVar, fromy, fromx, toy, tox)
• square_move(squareVar, fromy, fromx, toy, tox)
• point_move(parabolaVar, fromy, fromx, toy, tox)
• parabola_move(parabolaVar, fromy, fromx, toy, tox)
You need to remember four subroutine names (circle_move, square_move, point_move, and parabola_move), none of which is especially memorable. Now consider an object oriented language, where objects circle, square, point and parabola each implement their own move routine:
• circle.move(fromy, fromx, toy, tox)
• square.move(fromy, fromx, toy, tox)
• point.move(fromy, fromx, toy, tox)
• parabola.move(fromy, fromx, toy, tox)
In Object Oriented Programming (OOP), move means move -- it's intuitive.
Others will state additional benefits. They'll tell of the ability to redefine operators depending on the types being manipulated. They'll speak of inheritance, where you can create a new object type that's an enhancement of one already made, and you can even create a family of similar object types that can be manipulated by same named, similar performing subroutines. These are all nice, but in my opinion the only essentials are encapsulation and reduction of namespace collision.
Many tout OOP for purposes of reusability. I disagree. Everyone's talking about reusable code, but few are writing it, with OOP or anything else. Reusability is harder to find than the fountain of youth. If OOP were really that reusable, that wouldn't be true.
Classes and Objects
Think of a class as a set of architectural drawings for a house. Think of objects as the houses built according to those drawings. The drawings can be used as a plan for many, many houses. Not only that, the houses needn't be the same. Some can have carpeting, some have wood floors, but they were all created from the drawings. Once the house is created, the owner can put in a 14 cubic foot refrigerator or a 26 foot one. The owner can put in the finest entertainment center, or a 14" TV with rabbit ears on a wooden crate. No matter, they were all made from the same drawings. The drawing is the class, the house is the object.
A class is a plan to create objects. Ideally it lists all the data elements that will appear in any of its objects. It lists any subroutines the objects will need to manipulate the data. Those subroutines are called methods in OOP speak. It might even give the data elements initial values so that if the programmer doesn't change them, he has intelligent defaults. But typically, the computer program changes at least some of those data elements while it's being run.
Simple OOP in Ruby
In Ruby, a class begins with the class keyword, and ends with a matching end. The simplest class that can be made contains nothing more than the class statement and corresponding end:
class Myclass
end
The preceding class would not error out, but it does nothing other than tell the name of its class:
#!/usr/bin/ruby
class Myclass
end
myclass = Myclass.new
print myclass.class, "\n"
[slitt@mydesk slitt]$ ./hello.rb
Myclass
[slitt@mydesk slitt]$
To be useful, a class must encapsulate data, giving the programmer methods (subroutines associated with the class) to read and manipulate that data. As a simple example, imagine a class that produces objects that maintain a running total. This class maintains one piece of data, called @total, which is the total being maintained. Note that the at sign (@) designates this variable as an instance variable -- a variable in scope only within objects of this class, and persistent within those objects.
This class has a method called hasTotal() that returns true if the total is defined, false if it's nil. That way you can test to make sure you don't perform operations on a nil value. It also has getTotal() to read the total. It has setTo() to set the total to the argument of setTo(), it has methods increaseBy() and multiplyBy() add or multiply the total by an argument.
Last but not least, it has initialize()., which is called whenever Total.new() is executed. This happens because initialize() is a special reserved name -- you needn't do anything to indicate it's a constructor. The number of arguments in initialize() is the number of arguments Total.new() expects. The other thing that happens in initialize() is that all the instance variables are declared and initialized (in this case to the argument passed in through new().
Here is the code:
#!/usr/bin/ruby
class Total
def initialize(initial_amount)
@total=initial_amount
end
def increaseBy(increase)
@total += increase
end
def multiplyBy(increase)
@total *= increase
end
def setTo(amount)
@total = amount
end
def getTotal() return @total; end
def hasTotal() return @total!=nil; end
end
total = Total.new(0)
for ss in 1..4
total.increaseBy(ss)
puts total.getTotal if total.hasTotal
end
print "Final total: ", total.getTotal, "\n" if total.hasTotal
[slitt@mydesk slitt]$ ./hello.rb
1
3
6
10
Final total: 10
[slitt@mydesk slitt]$
The main routine instantiates an object of type Total, instantiating the total to a value of 0. Then a loop repeatedly adds the loop subscript to the total, printing each time after the add. Finally, outside the loop, the total is printed, which is 10, otherwise known as 1+2+3+4.
Take some time to study the preceding example, and I think you'll find it fairly self-explanatory.
Now for a little controversy. Remember I said you declare all instance variables inside initialize()? You don't have to. You could declare them in other methods:
#!/usr/bin/ruby
class Total
def initialize(initial_amount)
@total=initial_amount
end
def setName(name) @name = name; end
def hasName() return @name != nil; end
def getName() return @name; end
def increaseBy(increase)
@total += increase
end
def multiplyBy(increase)
@total *= increase
end
def setTo(amount)
@total = amount
end
def getTotal() return @total; end
def hasTotal() return @total!=nil; end
end
total = Total.new(15)
print total.getTotal(), "\n"
print total.getName(), "\n"
total.setName("My Total")
print total.getName(), "\n"
[slitt@mydesk slitt]$ ./hello.rb
15
nil
My Total
[slitt@mydesk slitt]$
From a viewpoint of pure modularity, readability and encapsulation, you'd probably want to have all instance variables listed in the initialize() method. However, Ruby gives you ways to access instance variables directly, either read-only or read-write. Here's a read only example:
#!/usr/bin/ruby
class Person
def initialize(lname, fname)
@lname = lname
@fname = fname
end
def lname
return @lname
end
def fname
return @fname
end
end
steve = Person.new("Litt", "Steve")
print "My name is ", steve.fname, " ", steve.lname, ".\n"
[slitt@mydesk slitt]$ ./hello.rb
My name is Steve Litt.
[slitt@mydesk slitt]$
You and I know fname and lname are accessed as methods, but because they're read as steve.fname, it seems like you're directly reading the data. Now let's go for a read/write example:
#!/usr/bin/ruby
class Person
def initialize(lname, fname)
@lname = lname
@fname = fname
end
def lname
return @lname
end
def fname
return @fname
end
def lname=(myarg)
@lname = myarg
end
def fname=(myarg)
@fname = myarg
end
end
steve = Person.new("Litt", "Stove")
print "My name is ", steve.fname, " ", steve.lname, ".\n"
steve.fname = "Steve"
print "My name is ", steve.fname, " ", steve.lname, ".\n"
When I instantiated the object in the preceding code, I accidentally spelled my name "Stove". So I changed it as if it were a variable. This behavior was facilitated by the def lname=(arg) method. The output of the preceding code follows:
[slitt@mydesk slitt]$ ./hello.rb
My name is Stove Litt.
My name is Steve Litt.
[slitt@mydesk slitt]$
The methods facilitating the seeming ability to write directly to the data are called accessor methods. Because accessor methods are so common, Ruby has a shorthand for them:
#!/usr/bin/ruby
class Person
def initialize(lname, fname)
@lname = lname
@fname = fname
end
attr_reader :lname, :fname
attr_writer :lname, :fname
end
steve = Person.new("Litt", "Stove")
print "My name is ", steve.fname, " ", steve.lname, ".\n"
steve.fname = "Steve"
print "My name is ", steve.fname, " ", steve.lname, ".\n"
[slitt@mydesk slitt]$ ./hello.rb
My name is Stove Litt.
My name is Steve Litt.
[slitt@mydesk slitt]$
In the preceding code, the variables after attr_reader substituted for the readonly accessor members, while the attr_writer substituted for the writeonly accessor members. Notice that when you write the names of the instance variables, you substitute a colon for the instance variables' at signs. There is actually a syntax reason, consistent with Ruby, for this substitution, but I can't explain it, so I choose to just remember it.
Remember, this seeming direct access must be explicitly enabled by the class's programmer, so this usually doesn't compromise encapsulation beyond what needs to be available. In my opinion this is a really handy option.
Inheritance
Inheritance is where a more specific kind of class is made from a more general one. For instance, an employee is a kind of person. Specifically (and oversimplistically), it's a person with an employee number. See this inheritance example:
#!/usr/bin/ruby
class Person
def initialize(lname, fname)
@lname = lname
@fname = fname
end
attr_reader :lname, :fname
attr_writer :lname, :fname
end
class Employee < Person # Declare Person to be parent of Employee
def initialize(lname, fname, empno)
super(lname, fname) # Initialize Parent's (Person) data
# by calling Parent's initialize()
@empno = empno # Initialize Employee specific data
end
attr_reader :empno # Accessor for employee specific data
attr_writer :empno # Accessor for employee specific data
# Parent's data already given accessors
# by parent class definition
end
steve = Employee.new("Litt", "Steve", "12345")
print steve.fname, " ", steve.lname, " is employee number ", steve.empno, ".\n"
[slitt@mydesk slitt]$ ./hello.rb
Steve Litt is employee number 12345.
[slitt@mydesk slitt]$
Ruby REALLY makes inheritance easy. On the class line you declare the child class's parent. In the child class's initialize() you call the parent's initializer by the super(supers_args) syntax. Because the parent's data is initialized and available to the child, you needn't redeclare accessor methods for the parent's data -- only for the child's data. In other words, in the child class you need code only for data specific to the child. It's handy, intuitive, and smooth.
Redefining Operators
It is nice to have total.add() and total.increaseBy() methods. But in many cases it's even more intuitive to use the + or += operator. In C++ it's always somewhat difficult to remember how to redefine operators. Not so in Ruby:
#!/usr/bin/ruby
class Total
def getTotal() return @total; end
def hasTotal() return @total!=nil; end
def initialize(initial_amount)
@total=initial_amount
end
def increaseBy(b)
@total += b
end
def add(b)
if b.class == Total
return Total.new(@total + b.getTotal())
else
return Total.new(@total + b)
end
end
def +(b)
self.add(b)
end
def *(b)
if b.class == Total
return Total.new(@total * b.getTotal())
else
return Total.new(@total * b)
end
end
end
total5 = Total.new(5)
total2 = Total.new(2)
total3 = Total.new(3)
myTotal = total5 + total2 + total3
print myTotal.getTotal(), "\n"
myTotal *= 2
print myTotal.getTotal(), "\n"
myTotal += 10
print myTotal.getTotal(), "\n"
In the preceding, we define add() as returning the argument plus @total. Notice that @total is not changed in-place. We might want to add add a Total to the existing Total, or we might want to add an integer. Therefore, Total::add() checks the argument's type, and if it's a Total it adds the argument's value, otherwise it adds the argument.
With add() safely defined, we now define + as basically a synonym for add(). The fascinating thing about Ruby is that if you define +, you get += free of charge, without further coding, and += does the right thing. As of yet I have not found a way to redefine +=, or any other punctuation string more than one character long. Luckily, += "just does the right thing", consistent with the definition of +.
It's not necessary to define a word function before redefining an operator, as the * operator (really a method) in the preceding code shows. Once again, it has an if statement so that integers or Totals can be added.
In the main part of the routine, we test by creating three totals with values 5, 2 and 3 repectively. We then add them together to create myTotal, which should be 10 and indeed is. We then in-place multiply by 2 to get the expected 20, and then in-place add 10 to get the expected 30:
[slitt@mydesk slitt]$ ./hello.rb
10
20
30
[slitt@mydesk slitt]$
No comments:
Post a Comment