How to Customise JWalk
Taking Control
Sometimes the tester may want to take control of how the
JWalk 1.1 Tool Suite
synthesises test inputs. Likewise, the tester may wish to establish
a particular context in which the test class is to be executed.
Both of these concerns are addressed by supplying one or more
custom generators and uploading them to the tool.
The JWalk 1.1 Tool Suite
provides a small library of ready-made custom generators.
Several of these are
loaded in the standard configuration at startup of the tools. Others
must be loaded explicitly by the tester. Here, we describe why you
might need a custom generator and how to build your own.
For help on how to upload a custom generator, please refer to the
main User Guide.
Master and Custom Generators
All of the tools in the JWalk 1.1 Tool Suite
synthesise test input values for the constructors and methods of the test
class using components called generators, which are Java classes
satisfying the Generator interface. Each generator is
capable of synthesising values (primitive values, or object instances) for
one or more types in the Java language. All generators must
synthesise their values in a predictable and repeatable way (rather than
randomly). The the tools in the
JWalk 1.1 Tool Suite
use a combination of
MasterGenerator and CustomGenerator generators to
synthesise all test inputs.
Master Generators
A MasterGenerator is any Java class that implements the
MasterGenerator interface. A MasterGenerator
uses a number of standard algorithms which generate a quasi-unique
value or object upon each request. It may also delegate requests to supply
values of certain types to one or more CustomGenerator s, if these
were previously added as delegates of that master. By default, the
MasterGenerator handles the synthesis of all types, but if a
CustomGenerator declares that it handles certain types, the
master will delegate to it instead. A delegate CustomGenerator
may synthesise values for one type, or for a group of types, using a different
custom algorithm.
The standard MasterGenerator used by the tools is
ObjectGenerator . This generator synthesises all basic values
in a quasi-unique monotonically increasing sequence. Values are said to be
"quasi-unique", since boolean values will inevitably cycle between
false and true . Values for other primitive types
are typically unique, for example, numeric values increase steadily from 1.
Object values will be uniquely instantiated using either their default
constructor, or the first constructor with arguments that succeeds (the
arguments are synthesised recursively). ObjectGenerator
delegates the synthesis of values for certain types to custom generators.
Custom Generators
A custom generator is any Java class that implements the
CustomGenerator interface. Such a class may be uploaded by
any of the tools in the JWalk 1.1 Tool Suite,
to take control of how input values are synthesised during testing.
Four standard custom generators are always uploaded by the
JWalk tools in the standard configuration, as delegates of
ObjectGenerator . These include:
StringGenerator - which generates a conventional sequence of strings
InterfaceGenerator - which generates concrete instances for interfaces
EnumGenerator - which generates all values of enumerated types
PlatformGenerator - which generates values with platfom-specific ranges
These take control of how specific types are instantiated. For example,
there is no automatic means in Java for supplying instances of interface
types, so this must be handled by a custom generator, which maps the
interface type to a suitable concrete type and then requests an instance
of this instead. Again, for the String type, the default
generation strategy would produce
an empty string, so a custom strategy is used to cycle through a series
of 26 conventional strings, sorted alphabetically.
Reasons for Customisation
If the tester supplies a home-made
custom generator, this can be made to synthesise values for particular
types in any order desired. In this way, it is possible to tailor the
testing process to meet the expectations of the test class.
Reasons for doing this might include:
- wanting to re-order the default sequence of test values
- wanting to specify a particular value sequence for a given type
- wanting to specify values for interface and abstract class types
- wanting to redirect standard input to read from a text file
- wanting to define closely the environment of the test class
For example, the default ObjectGenerator synthesises
int values in monotonically increasing order, starting from 1.
(The default strategy tries to supply values that will cause object states
to change, rather than stay unchanged; also the default strategy supplies
a different value on each request, so the user can see differences in
method results). However, you might want to test a class that expects a
different pattern of integer inputs on successive method calls.
In order to test the indexing behaviour of classes like Vector
or ArrayList , you might prefer to generate pairs of repeating
indices, starting with 0, in order to verify that put() and
get() operations store and fetch the same value at the same
index. For this, you might invent an IndexGenerator , which
takes control of synthesising int values (see also below).
For example, you might have a test class whose methods read
from standard input. This might be the case when testing the main class
for your application. Testing such a class exhaustively in the standard
tool configuration would require the tester to keep on entering input data
at the console, which is tedious. However, if you instead upload the
RedirectInGenerator , this redirects standard input to read
from a data file called input.txt , which by default contains
lines of text and integers. You can edit this file to provide
whatever inputs your test class expects (and maybe some which it does not
expect, to check your data validation). This custom generator
is provided in the download bundle, in the package org.jwalk.gen , and you can upload it from there into the tool.
For example, you might have a test class that needs to be plugged
in to a particular local environment. Your test class has a
constructor or method which sets up links to objects in the environment.
You can therefore provide a special ContextGenerator , which
returns instances of these context-objects, suitably initialised, for your
test class. Alternatively, you can arrange for your
ContextGenerator to create stub-, or mock-objects
which have the same name and API as the real context-objects, but which are
easier to set up and control for the purposes of testing. One strategy for
doing this in Java is to have both the context-object and mock-object
implement the same interface. Another is to override all methods of the
context-object in the mock-object, replacing production behaviour with test
behaviour.
Providing Custom Generators
If the tester wishes to supply original custom generators, the following
conventions must be observed. The custom generator must satisfy the interface
specified by CustomGenerator . In particular, it must implement
the following abstract methods:
boolean canCreate(Class<?> type) , reporting whether
the generator can create values of the requested type;
Object nextValue(Class<?> type) , to synthesise the
next value of the requested type in the expected order; and
void setOwner(MasterGenerator generator) , to register its
owning MasterGenerator.
A CustomGenerator may also choose to supply an explicit default
constructor (with no arguments). If none is explicitly provided, Java will
provide one automatically, which initialises all attributes of the generator
to default initial values, or user-defined initial values (see Java rules for
initialising values of different types).
The Contract for
boolean canCreate(Class<?> type)
This method reports whether the custom generator can synthesise instances
of the requested type. It should return true for all the
primitive, or class types
that the custom generator can handle, that is, for which it can generate
instances. This method is called internally, when deciding whether
to delegate a request to the custom generator. The method should return
true , if the custom generator creates instances of the requested
type, and false otherwise.
When the standard MasterGenerator receives a request to
synthesise a value of a particular type, it checks first whether any of its
delegates can create this value. If so, it forwards the request to the
first delegate found in its list of delegates that is capable of creating the
value. The list of delegates is ordered like a stack, such that delegates
that were added most recently take priority over delegates that were added
earlier by the MasterGenerator .
The Contract for
Object nextValue(Class<?> type)
This method returns the next value in sequence for the requested type.
Whereas by default this would be a quasi-unique, monotonically increasing
value on each invocation, custom generators are free to return values in
other orders, so long as the order is completely deterministic and not
random. So, the sequence could skip or re-order values, or revisit values
that were returned on a previous invocation. Random synthesis must be avoided, since
this would defeat the capability of the JWalk tools to learn predictable
behaviour.
The requested type must be a type that is acceptable to this custom
generator, i.e. one for which canCreate() returns
true . If not, the custom generator should raise a
GeneratorException reporting failure to synthesise a value
for the given type. The result of this method is an object, an array, or a
"boxed" primitive value.
The Contract for
void setOwner(MasterGenerator generator)
This method registers a MasterGenerator as the owner of this
custom generator, and is automatically called when this custom generator is
added as a delegate of the MasterGenerator . In this way, a
custom generator may communicate with the master which owns it. This can be
useful in the design of certain custom generators, which intercept requests
to handle certain types, but translate these into requests for different,
compatible types, delegating the modified requests back to their owner.
An example is InterfaceGenerator , which maps from abstract
interfaces to concrete classes, then recursively asks its owner to produce
an instance of the concrete class. (Note: you should not delegate a request
for the same type back to the owner - this would result in an
infinite delegation loop!)
A custom generator is free to ignore its owner if it so chooses. Many
custom generators need not communicate with their owner. In this case, a
nullop implementation of the method should be provided.
Example Custom Generators
The following is an example custom generator. IndexGenerator
takes control of how values are synthesised for the int type.
Instead of a monotonically increasing sequence, it generates repeating pairs
of integers, for use when testing array and sequence types, which may put
and get values at the same index. Repeating pairs of indices are generated,
so that it might be possible to observe that the value returned by a
get() method is the value that was previously stored by a
put() method at the same index.
IndexGenerator
import org.jwalk.GeneratorException;
import org.jwalk.gen.CustomGenerator;
import org.jwalk.gen.MasterGenerator;
public class IndexGenerator implements CustomGenerator {
private int seed = -1;
private boolean repeat = false;
public boolean canCreate(Class<?> type) {
return type == int.class;
}
public Object nextValue(Class<?> type)
throws GeneratorException {
if (type != int.class)
throw new GeneratorException(type);
if (repeat) {
repeat = false;
return new Integer(seed); // uses old value
}
else {
repeat = true;
result = new Integer(++seed); // uses new value
}
}
public void setOwner(MasterGenerator generator) {
} // nullop implementation
}
This IndexGenerator returns pairs of integer values in the
sequence: {0, 0, 1, 1, 2, 2, 3, 3, ...}. It works by synthesising
a new value if the flag repeat is set to false ,
otherwise it returns the previous value. The flag flips between
false and true on each call. The result is
returned as a boxed-up Integer object. This is un-boxed
automatically when supplied as an argument to any method expecting an
int index. Notice how the generator handles the synthesis
of integers locally and does not need to communicate with
its owner . The implementation of setOwner() is
therefore a null operation.
This IndexGenerator also starts synthesizing from 0, to
allow initial array indices to be accessed. The default sequence of integers
normally starts from 1, because 0 is an identity element for arithmetical
computations. JWalk's algebraic exploration will normally prune
test paths that result in no change to the test class, so the standard
value-synthesis algorithms try not to generate identity elements like 0.
However, when you know that you will be using integers for accessing array
indices, it makes more sense to start at zero.
The following is an example of a different kind of generator, which
synthesizes instances of a particular concrete DefaultListModel
type whenever an interface type ListModel is requested. This
is the pattern to follow, when dealing with the synthesis of concrete values
for abstract interface types. The supplied InterfaceGenerator
(which is always loaded, by default)
works this way for a number of commonly-used collection interfaces.
ListModelGenerator
import org.jwalk.GeneratorException;
import org.jwalk.gen.CustomGenerator;
import org.jwalk.gen.MasterGenerator;
public class ListModelGenerator implements CustomGenerator {
private MasterGenerator owner;
public boolean canCreate(Class<?> type) {
return type == ListModel.class;
}
public Object nextValue(Class<?> type)
throws GeneratorException {
if (type != ListModel.class)
throw new GeneratorException(type);
// return a suitable concrete type
return owner.nextValue(DefaultListModel.class);
}
public void setOwner(MasterGenerator generator) {
owner = generator;
}
Here, the custom generator needs to communicate with its
owner to handle the synthesis of the concrete type,
recursively. The generator therefore declares a field for its
owner , which is initialised by the method
setOwner() . Note that this mutually-recursive style of
delegating requests only works, because the requested type has been
changed. If you ask the owner for the same type,
this will result in a recursive loop, since the owning master generator
will delegate back to this custom generator ad infinitum.
|