Regression
Testing and the Saff Squeeze
Kent Beck, Three Rivers
Institute
(interested
in hiring Kent?)
Abstract: To
effectively isolate a defect, start with a system-level test and
progressively inline and prune until you have the smallest possible
test that demonstrates the defect.
private MaxCore fMax;
@Before public void createMax() {
fMax= MaxCore.createFresh();
}
public static class TwoOldTests extends TestCase {
public void testOne() {}
public void testTwo() {}
}
@Test public void junit3TestsAreRunOnce() throws
Exception {
Result result=
fMax.run(Request.aClass(TwoOldTests.class), new JUnitCore());
assertEquals(2,
result.getRunCount());
}
Running this test showed that four tests were being run, not
two. (The first version of the test ran JUnit 4-style tests and it
passed, leading to a minute or two of head scratching.) Here's where
the Sandwich Play came in. The test above hits the defect high, from
the point of view of a user. fMax.run()
. @Test public void saffSqueezeExample() throws
Exception {
Request request=
Request.aClass(TwoOldTests.class);
JUnitCore core= new JUnitCore();
//
fMax.run(request, core); --
inlined
core.addListener(fMax.new
RememberingListener());
Result result;
try {
result=
core.run(fMax.sortRequest(request).getRunner()); // We can assert right here
} finally {
try {
fMax.save();
} catch
(FileNotFoundException e) {
e.printStackTrace();
} catch
(IOException e) {
e.printStackTrace();
}
}
assertEquals(2,
result.getRunCount());
}
This made a big mess, but only temporarily. What I noticed is
that I could move the assertion immediately after the call to core.run()
.
Once I do that, all the code to save fMax
is irrelevant,
as is the listener. After pruning, here is the test that was left: @Test public void saffSqueezeExample() throws
Exception {
Request request=
Request.aClass(TwoOldTests.class);
JUnitCore core= new JUnitCore();
Result result=
core.run(fMax.sortRequest(request).getRunner());
assertEquals(2,
result.getRunCount());
}
core.run()
. And moved the assertion.
And pruned. And inlined... Eventually (after ~10 cycles) I had isolated
the method that was causing the problem: @Test public void saffSqueezeExample() throws
Exception {
final Description method=
Description.createTestDescription(TwoOldTests.class, "testOne");
Filter filter=
Filter.matchDescription(method);
JUnit38ClassRunner child= new
JUnit38ClassRunner(TwoOldTests.class);
child.filter(filter);
assertEquals(1,
child.testCount());
}
JUnit38ClassRunner can't filter its tests if they are plain
JUnit 3.8 tests. That's the problem. Now, if this was a fairy tale I'd
be able to tell you the simple fix that made everything work. Instead,
I'm still struggling with how in the world to fix that method. The Saff
Squeeze worked well enough, though, that I wanted to get it written up
right away. public void caller() {
boolean thrown= foo.callee();
...
}
Foo
private boolean callee() {
try {
return true;
} catch (Exception e) {
return false;
}
}
You can inline this safely by hand by replacing the return
statements with assignments: public void caller() {
boolean thrown;
try {
thrown= true;
} catch (Exception e) {
thrown= false;
}
...
}
callee()
: public void caller() {
boolean thrown= callee2();
...
}
private boolean callee2() {
foo.callee();
}
The reference to foo.callee() can now be inlined automatically: private boolean callee2() {
try {
return true;
} catch (Exception e) {
return false;
}
}
Now, because you are dealing with particular data values, you
can likely eliminate one side or the other of the try/catch block. This
lets you automatically inline callee2()
and continue
squeezing.