Thursday, December 26, 2013

Test Driven Development (TDD) recommends first writing a failing test.

Continuous Integration recommends committing early and often.

How do you do both and not break the build?

How do you distinguish between test failures associated with regressions and test failures associated with unfinished development?

Using Junit @Rules!

Instead of just disabling a test using the Junit standard @Ignore annotation, you can annotate a new test using @NotImplemented. The result of these tests will be inverted; the automated build's junit test will succeed if and only if the actual test logic fails.

That way, when the application functionality is still incomplete the actual failing test will not break the build. When the functionality is ready, the inverted test will start breaking the build, so that you don't forget to enable it.

And the @NotImplemented annotation provides a machine-checked way to track known issues.

Here's the implementation:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.*;

/*
 * To use in a testcase, include a line:
 *
        @Rule
        public NotImplemented.MustFail notImplementedRule = new NotImplemented.MustFail();
 *
 * and annotate individual test methods:
 *
        @NotImplemented @Test
        public void someTest() {
                ...
        }
 *
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NotImplemented {
        String value() default ""; //to enable grouping tests; doesn't affect tests
        boolean invertTest() default true;

        public static class MustFail implements TestRule {
                @Override
                public Statement apply(final Statement base, Description description) {
                        NotImplemented annotation = description.getAnnotation(NotImplemented.class);
                        if (annotation == null || !annotation.invertTest()) {
                                return base;
                        } else {
                                return new Statement() {
                                        @Override
                                        public void evaluate() throws Throwable {
                                                try {
                                                        base.evaluate();
                                                } catch (AssertionError e) {
                                                        return;
                                                }
                                                throw new AssertionError();
                                        }
                                };
                        }
                }
        }
}