The last year has seen me working with Ruby rather than Java and I have really come to like the the way tests (or rather specs) are structured in RSpec and the general way BDD approaches tests (specs).
RSpec splits the behaviour of a class up into what happens in different contexts. An example probably explains things better, taking a trivial case of a stack (I'll get to Java in a minute but even if you don't know Ruby I think this code should be understandable):
RUBY:
-
require File.dirname(__FILE__) + "/stack"
-
-
context "A populated Stack" do
-
setup do
-
@stack = Stack.new
-
["a","b","c"].each { |x| @stack.push x }
-
end
-
-
specify "should add to the top when sent #push" do
-
@stack.push "d"
-
@stack.peek.should == "d"
-
end
-
-
specify "should return the top item when sent #peek" do
-
@stack.peek.should == "c"
-
end
-
-
specify "should NOT remove the top item when sent #peek" do
-
@stack.peek.should == "c"
-
@stack.peek.should == "c"
-
end
-
-
specify "should return the top item when sent #pop" do
-
@stack.pop.should == "c"
-
end
-
-
specify "should remove the top item when sent #pop" do
-
@stack.pop.should == "c"
-
@stack.pop.should == "b"
-
end
-
end
-
-
context "An empty stack" do
-
setup do
-
@stack = Stack.new
-
end
-
-
specify "should be empty" do
-
@stack.should be_empty
-
end
-
-
specify "should no longer be empty after #push" do
-
@stack.push "anything"
-
@stack.should_not be_empty
-
end
-
-
specify "should complain when sent #peek" do
-
lambda { @stack.peek }.should raise_error(StackUnderflowError)
-
end
-
-
specify "should complain when sent #pop" do
-
lambda { @stack.pop }.should raise_error(StackUnderflowError)
-
end
-
end
-
-
context "An almost empty stack (with one item)" do
-
setup do
-
@stack = Stack.new
-
@stack.push 3
-
end
-
-
specify "should not be empty" do
-
@stack.should_not be_empty
-
end
-
-
specify "should remain not empty after #peek" do
-
@stack.peek
-
@stack.should_not be_empty
-
end
-
-
specify "should become empty after #pop" do
-
@stack.pop
-
@stack.should be_empty
-
end
-
end
So a stack should behave in a particular way:
A populated Stack
- should add to the top when sent #push
- should return the top item when sent #peek
- should NOT remove the top item when sent #peek
- should return the top item when sent #pop
- should remove the top item when sent #pop
An empty stack
- should be empty
- should no longer be empty after #push
- should complain when sent #peek
- should complain when sent #pop
An almost empty stack (with one item)
- should not be empty
- should remain not empty after #peek
- should become empty after #pop
(incidentally the above is output generated from the spec tool, a bit like generating JavaDocs from Java code)
I think splitting the behaviour up into these different contexts greatly improves the readability and maintainability of both the tests themselves and the code under test. But how to do it in Java?
It's possible to use RSpec to test Java code because you can run RSpec using JRuby, however I'm not sure if it is wise to drive the development of code in one language with tests in a different language, Ruby and Java approach numerous things very differently.
Now it would be possible to have numerous JUnit test classes for a given class, each one for a different context but this has it's drawbacks, when you make a change to your class you'd need to run numerous test cases, which you could get around by having a master testcase with a suite method that includes all the others, but then you have to maintain the list of testcases in the suite() method and it's all too easy for the list to get out of sync with the actual test cases. JUnit 4 provides a solution, you can use the Enclosed runner. The code below is an equivalent example to the RSpec one in Java.
JAVA:
-
import static org.junit.Assert.assertThat;
-
-
import static org.hamcrest.CoreMatchers.is;
-
-
import java.util.EmptyStackException;
-
-
import org.junit.Before;
-
import org.junit.Test;
-
import org.junit.runner.RunWith;
-
import org.junit.runners.Enclosed;
-
-
@RunWith(Enclosed.class)
-
public class TestStack {
-
-
public static class AnEmptyStack {
-
-
private Stack<Object> stack = null;
-
-
@Before
-
public void setupAnEmptyStack() {
-
stack = new Stack<Object>();
-
}
-
-
@Test
-
public void shouldBeEmpty() {
-
assertThat(stack.isEmpty(), is(false));
-
}
-
-
-
public void shouldComplainWhenPeeked() {
-
stack.peek();
-
}
-
-
-
public void shouldComplainWhenPoped() {
-
stack.pop();
-
}
-
-
@Test
-
public void shouldNotBeEmptyAfterItemIsPushedOntoIt() {
-
-
assertThat(stack.isEmpty(), is(false));
-
}
-
}
-
public static class AStackThatIsNeitherEmptyNorFull {
-
private Stack<String> stack = null;
-
-
@Before
-
public void setupAStackWithThreeItems() {
-
stack = new Stack<String>();
-
stack.push("one");
-
stack.push("two");
-
stack.push("three");
-
}
-
-
@Test
-
public void shouldAddToTheTopWhenSentAPush() {
-
stack.push("four");
-
assertThat(stack.peek(), is("four"));
-
}
-
-
@Test
-
public void shouldReturnTheTopItemWhenSentAPeek() {
-
assertThat(stack.peek(), is("three"));
-
}
-
-
@Test
-
public void shouldNotRemoveTheTopItemWhenSentAPeek() {
-
assertThat(stack.peek(), is("three"));
-
assertThat(stack.peek(), is("three"));
-
}
-
-
@Test
-
public void shouldReturnTheTopItemWhenSentAPop() {
-
assertThat(stack.peek(), is("three"));
-
}
-
-
@Test
-
public void shouldRemoveTheTopItemWhenSentAPop() {
-
assertThat(stack.pop(), is("three"));
-
assertThat(stack.peek(), is("two"));
-
}
-
}
-
public static class AStackThatContainsOnlyOneItem {
-
private Stack<String> stack = null;
-
-
@Before
-
public void setupAStackWithOneItem() {
-
stack = new Stack<String>();
-
stack.push("one");
-
}
-
-
@Test
-
public void shouldNotBeEmpty() {
-
assertThat(stack.isEmpty(), is(false));
-
}
-
-
@Test
-
public void shouldNotBeEmptyAfterAPeek() {
-
stack.peek();
-
assertThat(stack.isEmpty(), is(false));
-
}
-
-
@Test
-
public void shouldBeEmptyAfterAPop() {
-
stack.pop();
-
assertThat(stack.isEmpty(), is(true));
-
}
-
}
-
}
As an aside I've also used the new assertThat() method and Hamcrest matchers as you get significantly better error messages when your tests fail that way.
To get a plain test output you could probably write a XSLT stylesheet that parses the test report, the stylesheet Kerry produced for use with Junit 3.x tests maybe a good starting point, or write a custom formatter and specify that in your JUnit Ant task.
There are also the JBehave and JDave BDD frameworks for Java which would be worth looking at if you like the look of BDD.