Documentation
Intro
JDave is a framework which provides an easy way to specify behavior of your classes. Key concepts are specification, which is a container for behaviors. Behaviors define the behavior of a class and they are always expressed within some context. Context defines the setting where particular behavior applies.
For example, we could specify a Stack by first identifying that it has
(at least) three interesting states, an empty stack, a full stack and a stack which
is neither empty nor full. We would then continue by specifying the behavior of
a Stack when it is in these states. The behavior can then be expressed as:
Stack specification
Empty stack
- is empty
- is no longer empty after push
- ...
Full stack
- is full
- complains on push
- contains all items
- ...
Stack which is neither empty nor full
- adds to the top when pushing new item
- ...
Basics
A specification is implemented by extendingjdave.Specification base class.
It is parametrized by the type of object which is created in contexts. Contexts are
implemented as public non-static inner classes in the specification class. They do not
have to extend any base class or implement any interface. Context can optionally have a public
method create() which either returns a context object or is void. If create()
methods returns something, it is bound to variables context and be.
Context can also optionally have a public void method destroy().
All other public methods in context class are behaviors.
public class StackSpec extends Specification<Stack<?>> {
public class EmptyStack {
private Stack<String> stack;
public Stack<String> create() {
return stack = new Stack<String>();
}
public void someBehavior() {
// ...
}
}
}
Behaviors are written by doing something to the context object and
then specifying some expectations. The expectations are specified using one or
more overloaded specify(..., ...) methods.
public class StackSpec extends Specification<Stack<?>> {
public class EmptyStack {
// ...
public void isNoLongerEmptyAfterPush() {
stack.push("anything");
specify(stack, must.not().be.empty());
}
}
}
Specification base class provides a limited vocabulary to be used for writing
expectations. In the above example must is a supplemental word (*) which
does nothing. It is used to make expectation more readable. not() triggers
following expectations to be negations. be refers to the object
which was returned from create() method (be == create()).
(*) does and should can be used too, so these are equivalent:
specify(something, should.equal(...));
specify(something, does.equal(...));
specify(something, must.equal(...));
Expectations
Expectations are set usingspecify(..., ...) method.
- Basic:
specify(actual.getAge(), must.equal(20));
specify(actual, must.be.empty());
specify(actual, containsInOrder(1, 2, 3));
specify(actual, containsExactly(1, 2, 3));
specify(new Block() {}, must.not().raise(SomeException.class));
specify(new Block() {}, must.raise(SomeException.class, "message"));
specify(new Block() {}, must.raiseExactly(SomeException.class));
specify(new Block() {}, must.not().raiseAnyException());
specify(actual, satisfies(new SerializableContract()));
specify(actual, does.not().satisfy(new CloneableContract()));
import static org.hamcrest.Matchers.*;
specify(person.getAge(), is(greaterThan(30)));
// Using a shared matcher for all collection elements:
specify(persons, where(
new Each<Person>() {{ matches(item.getSurname(), is("Doe")); }}));
specify(persons, where(
new Each<Person>() {{ matches(item.getAge(), is(greaterThan(30))); }}));
specify(persons, where(
new Each<Person>() {{ matches(item, instanceOf(Person.class)); }}));
// Using individual matcher for each collection element, fails if
// number of matchers do not equal with number of elements:
specify(persons, where(
new Each<Person>() {{ matches(item.getAge(), is(35), is(31)); }}));
Lifecycle
A newSpecification and context instance is created before executing
each behavior. destroy() method is executed just after each behavior
method.
- spec = new SomeSpecification();
- spec.create();
- ctx = spec.new SomeContext();
- be = ctx.create(); // if exists
- [run some behavior]
- ctx.destroy(); // if exists
- spec.destroy();
- skip to 1. if there's more behaviors to be run
ILifecycleListener in a specification to get
notifications about lifecycle events.
Containments
Specification class provides expectation methods to compare various collection classes and their friends.containsAll, does.not().containAll
The actual collection must contain all given elements. The order of elements
is not significant.
containsAny, does.not().containAny
The actual collection must contain at least one element from given collection.
containsExactly, does.not().containExactly
The actual collection must contain exactly the same elements as in given collection.
The order of elements is not significant.
containsInOrder, does.not().containInOrder
The actual collection must contain exactly the same elements as in given collection
and in same order.
containsInPartialOrder, does.not().containInPartialOrder
The actual collection can hold other objects, but the objects which are common in
both collections must be in same order. The actual collection can also repeat
some elements. Example, [1, 2, 2, 3, 4] is in partial order with [1, 2, 3].
See Wikipedia for
further information.
specify(Arrays.asList(0, 1, 2), containsAll(Arrays.asList(0, 1)));
specify(Arrays.asList(1, 2).iterator(), containsAny(0, 1));
specify(new Object[] { 1, 2 }, does.not().containAny(0, -1)));
specify(new MyIterable(1, 2), containsExactly(2, 1)));
specify(new MyIterable(1, 2), containsInOrder(1, 2)));
specify(Arrays.asList(1, 2, 3), containsInPartialOrder(1, 2)));
etc.
Application specific containments can be done by implementing jdave.IContainment interface.
Map containments can be specified using maps(key1, key2, ...).to(value1, value2, ...) construct:
Map<Integer, String> actual = Maps.map(1, "foo").map(2, "bar");
specify(actual, maps(1, 2).to("foo", "bar"));
Contract enforcements
Some core Java classes impose (or recommend) contracts for classes which implement some interface or override a method. Probably most common is the contract betweenequals() and hashCode(). The following contract enforcements
are provided.
EqualsHashCodeContractSerializableContractCloneableContractEqualsComparableContract
satisfies method.
should.not().satisfy can be used if an object should not satisfy
a contract.
public void isSerializable() {
specify(serializable, satisfies(new SerializableContract()));
}
public void isNotSerializable() {
specify(nonSerializable, should.not().satisfy(new SerializableContract()));
}
Application specific contracts can be done by implementing jdave.IContract
interface.
Using mocks
JMock 2 is integrated as mocking framework. Please refer to JMock documentation.
Mocks are created with mock method and all mocks are automatically verified after the behaviour method
execution. Dummies can be created with dummy method. Dummy is a mock whose all methods are ignored.
JMock Mockery instance can be accessed using specification field mockery.
By default, JDave uses imposteriser which allows mocking of concrete classes. This default can
be changed with mockery.setImposteriser method.
The final keyword protects classes and methods from being overridden. Sometimes you want to mock a class or a method that has been declared final in a 3rd party library. this is not what final should be protecting against, so jdave-unfinalizer removes these restrictions on mocking.
To load jdave-unfinalizer, and thereby unfinalize classes not in the boot classloader, launch your tests with the following vm parameter:
-javaagent:/path/to/workspace/jdave-unfinalizer/target/jdave-unfinalizer-jar-name.jar
UnfinalizerAcceptanceSpec demonstrates the mocking of a final class and a final method, both previously impossible.
Extending default vocabulary
Extending the default vocabulary is done by overriding fields should, must and does. Then adding newspecify(...) and factory methods which provide the extension. Negation
can be extended by overriding not() method.
Let's go through an example which adds following extension to the vocabulary:
specify(a, should.approximate(b));
A possible implementation could be done by extending base Specification and encapsulating
the extension there. (Note, the extension can also be done as static methods if inheritance is too cumbersome.
The methods can then be statically imported to specification which needs them.)
public abstract class ExtendedSpecification<T> extends Specification<T> {
protected ExtendedSpecification<T> should = this;
protected ExtendedSpecification<T> does = this;
protected ExtendedSpecification<T> must = this;
public void specify(double actual, Approximation approximation) {
approximation.approximate(actual);
}
public Approximation approximate(double expected) {
return new Approximation(expected);
}
}
class Approximation {
private final double expected;
Approximation(double expected) {
this.expected = expected;
}
public void approximate(double actual) {
if (Math.abs(actual - expected) > 0.01) {
throw new ExpectationFailedException(actual +
" is not an approximation of " + expected);
}
}
}
To use the extension simply extend the specification from ExtendedSpecification.
See a complete example which supports not() too below:
[extension]
[usage of extension]
Injecting values into fields
InjectionSupport can be used to inject values into specification (or any other object) fields. This is often used for instance to inject Spring managed beans into specification.Thread local isolation
Some contexts set thread local variables. This may cause following behaviors to fail if they depend on initial thread local state. Thread locals can be isolated for all behavior methods of current specification by overriding the methodSpecification.needsThreadLocalIsolation() and returning true. Then a fresh new thread
is created for all methods in the specification.
Grouping
Specifications can be grouped by tagging them with @Group annotation. The annotation takes an array of group names as an argument:
@Group({ "slow", "database" })
public class MySpecification extends Specification<Foo> {
}
The tagged specifications can then be run with suite runners:
@RunWith(JDaveGroupRunner.class)
@Groups(include={ "slow" })
public class SuiteForSlowSpecs {
}
@RunWith(JDaveGroupRunner.class)
@Groups(include={ Groups.ALL }, exclude={ "slow" })
public class SuiteForNonSlowSpecs {
}
Reporting
Report generation can be activated by setting system propertyjdave.tools.specdox.format.
Possible values are txt or xml. Use a space limited list to generate dox in
multiple formats simultaneously. The target directory is configured by
setting system property jdave.tools.specdox.dir (default is target/jdave).
Sample report generated with txt format:
StackSpec: Empty stack - is empty - is no longer empty after push Full stack - is full - complains on push - contains all items Stack which is neither empty nor full - adds to the top when sent push
Generated XML files can be converted to HTML report using XSL styleheet. Install Saxon 8 (or some other XSLT 2.0 compliant processor). Then download specdox.xsl and dox2html.sh to a same directory. Process styleseet by running the script:
dox2html.sh [/path/to/spec/xml/dir/]
Sample HTML report:
Specdox can be generated using a maven plugin too (note, it is required to activate specdox
generation to generate xml format jdave.tools.specdox.format=xml). The generated HTML
will be added to maven reports when generating the site with 'mvn site'. The generated docs will have
source xrefs if used with maven-jxr-plugin:
<plugins>
<plugin>
<groupId>org.jdave</groupId>
<artifactId>jdave-report-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
</plugin>
</plugins>
See a sample pom.xml for how to configure specdox as part of a maven build.