interface Stack { void push(int value) throws StackFullException; int pop() throws StackEmptyException; int capacity(); int size(); boolean isEmpty(); boolean isFull(); int peek() throws StackEmptyException; } |
At first, we shall need to define a subclass or
randomunit.RandomizedTestCase.
Create a file named StackTest.java with the following code:
StackTest.java
import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } } |
This creates a normal JUnit test case. The second and the
third arguments in the super()
call need explanation. We just declared that we want the test to
execute 1000 random actions, and use a special logging strategy.
Logging is a crucial component if one needs to trace the cause
of a bug. Also, log contents are printed by default, when a
failure occurs, so it is important to choose an adequate
LogStrategy implementation. LogStrategy definition is shown
here:
randomunit.LogStrategy
interface LogStrategy { void appendLog(MethodInvocationLog log); String dump(); } |
randomunit.MethodInvocationLog
is a class that has the corresponding
java.lang.reflect.Method instance of the method of the
test that was executed, along with actual parameters and
returned value. Available LogStrategy implementations are
discussed later. Please note though that RandomUnit's "logging"
concept is independent of general purpose logging frameworks,
like log4j. Integration is possible through the LogStrategy
interface.
Back to the example. We are ready to start creating test
methods. Lets assume that we have implemented
Stack in a class named StackImpl, as follows:
StackImpl.java
public class StackImpl implements Stack { public StackImpl(int capacity) { ... } ... //implementation methods omitted } |
Lets create a method that creates Stack instances with random
capacities:
StackTest.java
import randomunit.Creates; import randomunit.Prob; import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } @Prob(1) @Creates("stacks") public Stack randomNewStack() { int capacity = this.random.nextInt(10); return new StackImpl(capacity); } } |
The @Prob annotation declares
that this method should be executed randomly. It also specifies
the probability which is used when selecting a method to
execute. The probabilities are relative, in the sense that if we
have two methods with equal probability (not necessarily 0.5),
they should run 50%-50%, and so on. (More can be expressed with
a @Prob annotation, a topic to be discussed later). The
@Creates annotation declares
that when this method runs, whatever is the return value, store
it in an object pool named "stacks". The method itself
creates stacks of random capacity, in the range [0, 10).
Variable random is actually an inherited Random instance
used by the randomized test, declared with protected access, and
can be used to create random numbers. It is always seeded at a
constant number, so the tests (and their exact failures) are
reproducable.
In this trivial example, a stack doesn't really care about its
contents: any int will do.
But since this should rarely be the case in practice, the framework
could not simply create random integers and use them - we need
to create ourselves the ints
that we will use:
StackTest.java
import randomunit.Creates; import randomunit.Prob; import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } @Prob(1) @Creates("stacks") public Stack randomNewStack() { int capacity = this.random.nextInt(10); return new StackImpl(capacity); } @Prob(1) @Creates("ints") public int randomNewInt() { return random.nextInt(1000); } } |
These were just simple factory methods. Lets move on to the
interesting part and do some testing. We will start by pushing some integers in
stacks and see if anything is broken:
StackTest.java
import randomunit.Creates; import randomunit.Prob; import randomunit.Params; import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } @Prob(1) @Creates("stacks") public Stack randomNewStack() { int capacity = this.random.nextInt(10); return new StackImpl(capacity); } @Prob(1) @Creates("ints") public int randomNewInt() { return random.nextInt(1000); } @Prob(2) @Params({"stacks", "ints"}) public void randomTestPush(Stack stack, int value) { precondition(!stack.isFull()); int previousSize = stack.size(); stack.push(value); postcondition(!stack.isEmpty()); postcondition(stack.size() == previousSize + 1); postcondition(stack.peek() == value); } } |
Much is going on here. Lets start with the
@Params annotation. It specifies
a list of names of object pools, from which to pick random
elements and use it as parameters for this method. So, this is
actually a form of dependency-injection: the method declares
what objects it needs to do its testing, and the framework
provides them at runtime. As you may have guessed, these objects
will be some from the ones we create with the two creator
methods. Note that if a non-existent object pools is specified (ie,
there is no creator method that inserts elements to such a
pool), an exception is thrown that explains the fact; this is
helpful to prevent errors by typos in the annotations.
So, with the parameter injection mechanism, the test itself has
hardly any reason to store state; everything is provided as
method parameters, which makes the coding cleaner and more
readable. Also, please note that the parameter types do not
matter, as long as the randomly selected object can be injected
in the parameter; for example, we could also use "Object" as the
type of the first parameter, and "Integer", "Number",
"Comparable" etc for the second object.
The method implementation has to check its parameters at the
start of its testing; remember that the objects are chosen
randomly from their container pools, so the could just not be
applicable to the specific test method. We declare the
preconditions that must hold for us to actually perform a test.
If a precondition fails, the method is skipped like it was never
happened. If we wanted to test that when a stack is full,
attempt to push something throws an exception, then the
precondition of the test would be the exact opposite:
precondition(stack.isFull());
So we would essentially filter out non-full stacks for
this test.
Then, we proceed with the actual testing. We push a value, after
we record its current size. If an exception occurs here, we have
found a bug - a valid, non-full stack that does not accepts an
element. Then, we test the postconditions that we would like to
have. The stack cannot be empty after a push, the stack grows in
size by one, and when we peek the top element, we get back the
value that we just pushed. If any of these fails, we found
another bug.
Note that we do not call any
assertXXX method of JUnit. We could have (an assertion
error would be treated as a bug), but using precondition() and
postcondition() communicates the purpose of the assertion in a
much better and natural way.
What if we wanted to create objects based on other, existing
objects? No trouble at all - just combine @Creates with @Params,
and custom method parameters. The following would create stacks
with capacity equal to some other stack:
StackTest.java
@Prob(1) @Creates("stacks") @Params("stacks") public Stack randomNewStack(Stack stack) { return new StackImpl(stack.capacity()); } |
In this example, this would hardly provide any value, but
imagine how one could tap this expressiveness to create
elaborate inter-dependent objects. Note: obviously, at
least on creator method must take zero arguments and have
positive probability, and every object pool eventually should be
able to obtain objects. If a method with arguments is picked for
execution, and some object pool on which it depends is empty,
the method execution is skipped.
Another interesting point is that we can add a created object
into many pools at once. @Creates actually takes an array
of pool names. This could be done as following:
StackTest.java
@Prob(1) @Creates( { "stacks", "fancyStacks" } ) @Params("stacks") public Stack randomNewStack(Stack stack) { return new StackImpl(stack.capacity()); } |
Another aspect we would like to check is invariants. We could
simply create a method that accepts a Stack and tests its
invariants, and remember to call it in everymethod that we use a
stack, but this would be error-prone (the call could be omitted)
and needlessly tiresome. We can mark special methods that
check invariants so the framework can do the cumbersome work for
us:
import randomunit.Invariant; import randomunit.Creates; import randomunit.Prob; import randomunit.Params; import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } @Prob(1) @Creates("stacks") public Stack randomNewStack() { int capacity = this.random.nextInt(10); return new StackImpl(capacity); } @Prob(1) @Creates("ints") public int randomNewInt() { return random.nextInt(1000); } @Prob(2) @Params({"stacks", "ints"}) public void randomTestPush(Stack stack, int value) { precondition(!stack.isFull()); int previousSize = stack.size(); stack.push(value); postcondition(!stack.isEmpty()); postcondition(stack.size() == previousSize + 1); postcondition(stack.peek() == value); } @Invariant("stacks") public void checkStackInvariant(Stack stack) { invariant(stack.isEmpty() ^ stack.size() > 0); invariant(stack.isFull() ^ stack.size() < stack.capacity()); if (!stack.isEmpty()) { stack.peek(); //should not throw exception } } } |
This enforces the invariants of the stack, namely:
- It can be either empty, or its size must be greater than zero
(exclusive or)
- It can be either full, or its size must be less than its
capacity (exclusive or)
- If it is not empty, we can peek without getting an exception
@Invariant annotation
accepts a single object pool name, and the annotated method must
take exactly one corresponding parameter. We can define as many
@Invariant methods as we like,
even for the same object pool. All invariant checking methods
that apply to an object pool are checked against:
- Every object that is added to its pool
- Every object that has just been used as an injected parameter
in a random test method (after the method's completion).
These rules are sufficient to assure that every object has
tested invariants before uses of it. Of course, this
would not be the case if the objects that collaborate in test
are not taken as injected parameters. As expected, a failed
invariant indicates a bug, and an explaining exception is
thrown.
What would happen if we take a handle on an arbitrary object,
neither taken from the parameter list of the method nor returned
by the method? We might want to apply invariant checks against
such an object, and these checks cannot happen automatically
(it's an arbitrary object, not relevant to this method
execution, from the framework's point of view). A simple utility
is provided, that can be invoked like this:
StackTest.java
@Prob(1) @Params("stacks") public void randomStackFriend(Stack stack) { Stack friend = findMyFriendStack(stack); checkInvariants(friend, "stacks"); } |
In the example, we first create a reference to another Stack
instance (somehow, it doesn't matter), and then we call the
checkInvariants inherited method, providing as arguments the
object and the pool name with the desired associated invariants
checks. This results in the invocation of every @Invariant("stacks")
method that we have defined against the provided object.
import randomunit.Invariant; import randomunit.Creates; import randomunit.Prob; import randomunit.Params; import randomunit.RandomizedTestCase; import randomunit.SimpleLogStrategy; public class StackTest extends RandomizedTestCase { public StackTest(String testName) { super(testName, 1000, new SimpleLogStrategy(5)); } @Prob( { 70, 0 } ) @Creates("stacks") public Stack randomNewStack() { int capacity = this.random.nextInt(10); return new StackImpl(capacity); } @Prob( { 30, 0 } ) @Creates("ints") public int randomNewInt() { return random.nextInt(1000); } @Prob( { 0, 1 } ) @Params({"stacks", "ints"}) public void randomTestPush(Stack stack, int value) { precondition(!stack.isFull()); int previousSize = stack.size(); stack.push(value); postcondition(!stack.isEmpty()); postcondition(stack.size() == previousSize + 1); postcondition(stack.peek() == value); } @Invariant("stacks") public void checkStackInvariant(Stack stack) { invariant(stack.isEmpty() ^ stack.size() > 0); invariant(stack.isFull() ^ stack.size() < stack.capacity()); if (!stack.isEmpty()) { stack.peek(); //should not throw exception } } } |
For the first phase, only the two creator methods have
positive probabilities, and during the second phase, only
the randomTestPush has a
positive probability (so it will be always selected).
This is not enough, though. We need to tell the
framework to switch phases at some specific time. A nice
place is to override the callback method which is defined
(as protected) in RandomizedTestCase:
protected void onStep(int executedSteps) { } |
So, we can say for example, that when executedSteps ==
30, switch phase to 1. We can do it like this:
protected void onStep(int executedSteps) { if (executedSteps == 30) { //switch to phase 1 this.setPhase(1); } } |
The method setPhase(int) is naturally defined in
RandomizedTestCase.
3.3 Further Customization
Some more useful methods are defined, which can be
overrided to further customize a test.
You can centrally control (or monitor, or whatever) which
objects are added into pools by overriding the definition of
this method:
protected
Object filterNewObject(String pool, Object o) throws
PreconditionFailedException { return o; } |
For instance, you can exclude odd integers by this code:
protected
Object filterNewObject(String pool, Object o) { if (o instanceof Integer) { Integer i = (Integer)o; precondition(i % 2 == 0); } return o; } |
Or you may replace the created object by another
arbitrary object.
Another method you can override is this:
protected void examineException(TestFailedException e) { } |
This is called just before an error report is generated.
You could programmatically examine the log that is included
in the exception. Or, if you need to set a breakpoint in a
debugger, this would be a good candidate!
You can have full control over the object pools too. These
methods are provided:
protected Set<String> getPoolNames(); protected List<Object> getPool(String name); |
So, you could even load the pools with predefined
objects. Yet, a compromise is being made: you can't create
arbitrary pools; pools are only created when a method tagged
with @Creates is found. Were the creation of pools allowed
arbitrarily, a typo error in the declaration of @Params
could be only found during runtime - in this version, these
types of errors are reported at the construction of the
test. A work-around is to declare a creator method with
@Prob(0), and preload the pools with objects in the setUp()
method of JUnit.