September 9, 2012

Allowing JUnit Tests to Pass Test Case on Failures


Why create a mechanism to expect a test failure?

There comes a time when one would want and expect a JUnit @Test case fail. Though this is pretty rare, it happens. I had the need to detect when a JUnit Test fails and then, if expected, to pass instead of fail. The specific case was that I was testing a piece of code that could throw an Assert error inside of a call of the object. The code was written to be an enhancement to the popular new Fest Assertions framework, so in order to test the functionality, one would expect test cases to fail on purpose.


A Solution

Jump to the Code
One possible solution is to utilize the functionality provided by a JUnit @Rule in conjunction with a custom marker in the form of an annotation.


Why use a @Rule?

@Rule objects provide an AOP-like interface to a test class and each test cases. Rules are reset prior to each test case being run and they expose the workings of the test case in the style of an @Around AspectJ advice would.

Required code elements

  • @Rule object to check the status of each @Test case
  • @ExpectedFailure custom marker annotation
  • Test cases proving code works!
  • Optional specific exception to be thrown if annotated test case does not fail

NOTE: working code is available on my github page and has been added to Maven Central. Feel free to Fork the project and submit a pull request

Maven Usage

<dependency>
    <groupId>com.clickconcepts.junit</groupId>
    <artifactId>expected-failure</artifactId>
    <version>0.0.9</version>
</dependency>

Example Usage

In this example, the "exception" object is a Fest assertion enhanced ExpectedException (look for my next post to expose this functionality). The expected exception will make assertions and in order to test those, the test case must be marked as @ExpectedFailure

public class ExceptionAssertTest {

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Rule
    public ExpectedTestFailureWatcher watcher = ExpectedTestFailureWatcher.instance();

    @Test
    @ExpectedFailure("The matcher should fail becasue exception is not a SimpleException")
    public void assertSimpleExceptionAssert_exceptionIsOfType() {
        // expected exception will be of type "SimpleException"
        exception.instanceOf(SimpleException.class);
        // throw something other than SimpleException...expect failure
        throw new RuntimeException("this is an exception");
    }
}

Implementation of Solution

Reminder, the latest code is available on my github page.

@Rule code (ExpectedTestFailureWatcher.java)

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
// YEAH Guava!!
import static com.google.common.base.Strings.isNullOrEmpty;

public class ExpectedTestFailureWatcher implements TestRule {

    /**
     * Static factory to an instance of this watcher
     *
     * @return New instance of this watcher
     */
    public static ExpectedTestFailureWatcher instance() {
        return new ExpectedTestFailureWatcher();
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                boolean expectedToFail = description.getAnnotation(ExpectedFailure.class) != null;
                boolean failed = false;
                try {
                    // allow test case to execute
                    base.evaluate();
                } catch (Throwable exception) {
                    failed = true;
                    if (!expectedToFail) {
                        throw exception; // did not expect to fail and failed...fail
                    }
                }
                // placed outside of catch
                if (expectedToFail && !failed) {
                    throw new ExpectedTestFailureException(getUnFulfilledFailedMessage(description));
                }
            }

            /**
             * Extracts detailed message about why test failed
             * @param description
             * @return
             */
            private String getUnFulfilledFailedMessage(Description description) {
                String reason = null;
                if (description.getAnnotation(ExpectedFailure.class) != null) {
                    reason = description.getAnnotation(ExpectedFailure.class).reason();
                }
                if (isNullOrEmpty(reason)) {
                    reason = "Should have failed but didn't";
                }
                return reason;
            }
        };
    }
}

@ExpectedFailure custom annotation (ExpectedFailure.java)

import java.lang.annotation.*;

/**
 * Initially this is just a marker annotation to be used by a JUnit4 Test case in conjunction
 * with ExpectedTestFailure @Rule to indicate that a test is supposed to be failing
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface ExpectedFailure {
    // TODO: enhance by adding specific information about what type of failure expected
    //Class assertType() default Throwable.class;

    /**
     * Text based reason for marking test as ExpectedFailure
     * @return String
     */
    String reason() default "";
}

Custom Exception (Optional, you can easily just throw RuntimeException or existing custom exception)

public class ExpectedTestFailureException extends Throwable {
    public ExpectedTestFailureException(String message) {
        super(message);
    }
}

Can't one exploit the ability to mark a failure as expected?

With great power comes great responsibility, it is advised that you do not mark a test as being @ExpectedFailure if you do not understand exactly why the test if failing. It is recommended that this testing method be implemented with care. DO NOT use the @ExpectedFailure annotation as an alternative to @Ignore

Possible future enhancements could include ways to specify the specific assertion or the specific message asserted during the test case execution.

Known issues

In this current state, the @ExpectedFailure annotation can cover up additional assertions and until the future enhancements have been put into place, it is advised to use this methodology wisely.

3 comments:

Java Tutorials said...

Add JUnit support in Eclipse project
hi mike, this is crazy stuff. Thanks for posting the code. I am trying to make this work in my project. Once done, will write a blog post on my findings.

Nichole Williams said...

Usually, I never comment on blogs but your article is so convincing that I never stop myself to say something about it. You’re doing a great job Man, Keep it up.
Buy Logo

Haris said...

I feel satisfied with your share.
It's very helpful for me and I am so happy to see your blog.
Thank you for sharing it with us. law dissertation help