|
Chapter 10. Software Engineering and Frameworks Evolution Framework
growthe New
domains Bug
fixes The
conflict o o \
/ o Business models Razors
vs blades Source Documentation Testing How
to test abstract classes? [Make
concrete] What's
a test case ? [an application] Software engineering of Frameworks Iteration Evolution Domains Bug
fixes Conflicts Business model Razor
vs blades Source
or not Documentation Testing Abstract
classes Test
case = application Software Engineering of Frameworks Iteration Evolution Domains Bug
fixes Conflicts Biz model Razor
vs blades Source Documentation Testing Abstract
classes Test
case = app Iteration and Evolution Frameworks don't spring up in one piece "by
unaided thought alone." They develop through a reflective process,
reflecting the tension between frameworks and their application. <
- gives ideas to - Framework Application -
supports --> As the framework is used for applications, the
applications will show patterns of use that can be incorporated back into the
framework. This proceeds over time, and is a necessary part of
framework development. A framework is best generalized from several
applications. A framework evolves for the "usual" software
engineering reasons: to adapt to new environments, to enhance functionality, to
repair defects. What happens when the framework is changed? The
problem is that not everybody will want to use the new version: * the old version is used in an application that is
already deployed * the new version has new bugs * it takes work to update. For example, consider our first web site manager.
Suppose there is version 1, which uses the Graph class that has a bug in it,
and a framework. Suppose it grows to version 2, where the bug is fixed and a
plugin architecture is used. The framework user of version 1 would like to see
the bug fixed, but may not want to rewrite their part as a plugin. The biggest problems often come because of bugs.
Version 1 has certain bugs, which version 2 fixes, but version 2 changes other
things. Customers of version 1 may want a bug-free version 1 (which never
existed). It's hard to do this from an engineering/management perspective. Managing this usually requires tool support, from a
source code control system. This lets us run multiple versions of software: v1.0
- 1.1 - 1.2 | v2.0
- 2.1 2.2 2.3 (where does bug fix go?) This is a lot of work, and easy to get wrong. For
example, suppose a key data structure changes from v1.0 to v2.0; how do we
match up the bug fixes? Business Models Frameworks cost time and money to develop. How do you
ensure the result justifies the investment? The key consideration is that the cost of the
framework should be amortized over a number of applications, because the
benefits accrue to several applications. Some authors (ref) have advocated a split between
framework developers and application developers (some recommend two or three
teams). The framework authors try to provide frameworks that the application
team wants. What do you sell when you "sell" your
framework? Usually, it's a license to use. Do you sell source code or not? Some shops don't want
to depend on you being around to fix the bugs, or they need to see how things
work. Are you selling razors or blades? Consider web servers
- there are perfectly good servers available for free (e.g., Apache). Many
people can give away the web server (the razor) and sell things that use it
(the blades). All the money's in selling the blades. (This may be services as
well as actual products.) Testing Framework level = app Package level Abstract class Class Method Testing a framework involves concerns at a number of
levels: *
Framework and its applications *
[Package Level] *
Abstract Class *
Concrete Class *
Subclasses *
Methods *
[Interface] In some sense, the test case for a framework is an
application. Testing attempts to "break" the software: do
something documented to work one way, but the system performs another way. Framework testing: The hardest part is that the test case for a framework
is an application (a use of the framework). To test, we will build an
application and test the framework through the application. This gives you a chance to do "system level"
tests of the framework and its documentation. Your documentation should
have a minimal example of using the framework, and it should work as
advertised. [Key] If it's not defined, it can't be tested. Class Level testing: In object-oriented systems, we often can test classes
bottom-up. It is sometimes feasible to develop a testing package for each
class. When you're lucky, you can test each one in isolation. One special problem you face is in dealing with
abstract classes. Since the point of the abstract class is to omit things
(deferred to subclasses), the only way to test them is to subclass them with a
concrete class, and test the concrete class. You may introduce new concrete classes not part of the
framework. The Design by Contract constraints will help in this
testing. It's hard to test a class in isolation. You often need
to test the client too. If there are problems, it can be hard to tell if it's
the class or the client. [Demo?] * Create an instance. Test visible aspects of the
initial configuration. * Call modifiers. After modification, use accessors to
verify configuration. * Verify each method, each visible variable. * Work from public to protected to private. It's tricky to test private variables or methods.
Recall that in Java, items in the same package have access to private values.
You might introduce a test class into the package to do this. You need to
document the dependency. Perhaps you can use an inner class. Some classes will
just require scaffolding that may need to be removed later. It's worth applying some intelligence in the
testing - if a private variable is
merely behind simple public getter/setter methods, simply testing the public
methods is enough. Subclass: Test other relations Decouple Testing Methods: *Write test cases that will stress the method. There
are various kinds of coverage you can strive for: + all
statements executed + all
branches tried both ways + all methods
excecuted An effective way is to try corner cases. For integers,
this might be a large negative number, -1, 0, 1, and a large positive number. Stress Testing In addition to a straightforward client, it is
sometimes helpful to stress test a service. For a stress test, you want a
client that can call the class many times. This can be tricky to write. For
example, a stack class can't do a random sequence of pushes and pops (without
violating preconditions). The client would have to be smart enough to pop only
there are values on the stack. Ideally, you have some kind of oracle. An
oracle is a magic box that tells you if the object under test is correct. In
the worst case, the oracle is a re-implementation of the service. In other
cases, your oracle isn't all-knowing. Example TestingStack extends Vector { public
Stack() public
boolean empty() public
synchronized Object peek() public
synchronized Object pop() public
Object push(Object o) public
synchronized int search (Object o) // dist from top; -1=not present } 0 == 1 == many First, look at the modifiers only. The accessors
should have no effect. (new Stack()).empty() => true s.peek() => error s.pop() => error s.push(o1) => s' s.empty(s.push(o1)) => false s.peek(s.push(o1)) => o1 etc. Building an Oracle When we test, we need to decide if the test passes or
fails. In simple cases, we just compute the answer by hand
and use a one-shot oracle, e.g., empty(pop(push(stack)))
= true The problem with this is that we have to compute many
answers separately. Sometimes, there's an alternative. We may have a
separate implementation. For example, the JDK 1.2 collection classes provide
multiple implementations of things like Set. So if we put the same operation to
both sets, then we should get the same answer. If they don't agree, there's an
error somewhere. What can go wrong? * Non-determinism. For example, sets might provide
Enumerations in different orders. * Oracles can share failure modes. The same people
might have written both implementations, or there can be ambiguities in the
specification. * It's hard to find an oracle. An alternative is a reduced oracle. In some cases, we
can make a "projection" of an oracle that will test only part of the
state. For example, a stack oracle might track the stack
height, but not the values in it. After a series of operations, we can pop all
values, and test that the stack is empty when the oracle's count is 0. Another example might be a data processing program. It
might count records read and records written, and raise a flag if they don't
match. The simplest oracle is "no crash" - there's
no explicit oracle, we just try a lot of operations and make sure the service
doesn't crash. Once we have an oracle, we can do a stress test: Stack stack = new Stack(); for (int i = 0; i < numiters; i++) { float
f = random(0,1); if (f
< 0.4) { if
(!stack.empty()) { value
= stack.peek(); value2
= stack.pop(); if
(value != value2) error(); }
} else { stack.push(value3); } } |
|
Copyright 1994-2006, William C. Wake - William.Wake@acm.org |