/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test;

import junit.framework.Assert;
import junit.framework.AssertionFailedError;

/**
 * Simple method object encapsulation of the 'test-for-Exception' scenario (for JUnit).
 *
 * <p>Used like so:
 *
 * <pre class="code">
 * // the class under test
 * public class Foo {
 *    public void someBusinessLogic(String name) {
 *        if (name == null) {
 *            throw new IllegalArgumentException("The 'name' argument is required");
 *        }
 *        // rest of business logic here...
 *    }
 * }</pre>
 *
 * The test for the above bad argument path can be expressed using the
 * {@link AssertThrows} class like so:
 *
 * <pre class="code">
 * public class FooTest {
 *    public void testSomeBusinessLogicBadArgumentPath() {
 *        new AssertThrows(IllegalArgumentException.class) {
 *            public void test() {
 *                new Foo().someBusinessLogic(null);
 *            }
 *        }.runTest();
 *    }
 * }</pre>
 *
 * This will result in the test passing if the <code>Foo.someBusinessLogic(..)</code>
 * method threw an {@link java.lang.IllegalArgumentException}; if it did not, the
 * test would fail with the following message:
 *
 * <pre class="code">
 * "Must have thrown a [class java.lang.IllegalArgumentException]"</pre>
 *
 * If the <b>wrong</b> type of {@link java.lang.Exception} was thrown, the
 * test will also fail, this time with a message similar to the following:
 *
 * <pre class="code">
 * "junit.framework.AssertionFailedError: Was expecting a [class java.lang.UnsupportedOperationException] to be thrown, but instead a [class java.lang.IllegalArgumentException] was thrown"</pre>
 *
 * The test for the correct {@link java.lang.Exception} respects polymorphism,
 * so you can test that any old {@link java.lang.Exception} is thrown like so:
 *
 * <pre class="code">
 * public class FooTest {
 *    public void testSomeBusinessLogicBadArgumentPath() {
 *        // any Exception will do...
 *        new AssertThrows(Exception.class) {
 *            public void test() {
 *                new Foo().someBusinessLogic(null);
 *            }
 *        }.runTest();
 *    }
 * }</pre>
 *
 * You might want to compare this class with the
 * {@link junit.extensions.ExceptionTestCase} class.
 *
 * <p>Note: This class requires JDK 1.4 or higher.
 *
 * @author Rick Evans
 * @author Juergen Hoeller
 * @since 2.0
 */
public abstract class AssertThrows {

	private final Class expectedException;

	private String failureMessage;

	private Exception actualException;


	/**
	 * Create a new instance of the {@link AssertThrows} class.
	 * @param expectedException the {@link java.lang.Exception} expected to be
	 * thrown during the execution of the surrounding test
	 * @throws IllegalArgumentException if the supplied <code>expectedException</code> is
	 * <code>null</code>; or if said argument is not an {@link java.lang.Exception}-derived class
	 */
	public AssertThrows(Class expectedException) {
		this(expectedException, null);
	}

	/**
	 * Create a new instance of the {@link AssertThrows} class.
	 * @param expectedException the {@link java.lang.Exception} expected to be
	 * thrown during the execution of the surrounding test
	 * @param failureMessage the extra, contextual failure message that will be
	 * included in the failure text if the text fails (can be <code>null</code>)
	 * @throws IllegalArgumentException if the supplied <code>expectedException</code> is
	 * <code>null</code>; or if said argument is not an {@link java.lang.Exception}-derived class
	 */
	public AssertThrows(Class expectedException, String failureMessage) {
		if (expectedException == null) {
			throw new IllegalArgumentException("The 'expectedException' argument is required");
		}
		if (!Exception.class.isAssignableFrom(expectedException)) {
			throw new IllegalArgumentException(
					"The 'expectedException' argument is not an Exception type (it obviously must be)");
		}
		this.expectedException = expectedException;
		this.failureMessage = failureMessage;
	}


	/**
	 * Return the {@link java.lang.Exception} expected to be thrown during
	 * the execution of the surrounding test.
	 */
	protected Class getExpectedException() {
		return this.expectedException;
	}

	/**
	 * Set the extra, contextual failure message that will be included
	 * in the failure text if the text fails.
	 */
	public void setFailureMessage(String failureMessage) {
		this.failureMessage = failureMessage;
	}

	/**
	 * Return the extra, contextual failure message that will be included
	 * in the failure text if the text fails.
	 */
	protected String getFailureMessage() {
		return this.failureMessage;
	}


	/**
	 * Subclass must override this <code>abstract</code> method and
	 * provide the test logic.
	 * @throws Exception if an error occurs during the execution of the
	 * aformentioned test logic
	 */
	public abstract void test() throws Exception;


	/**
	 * The main template method that drives the running of the
	 * {@link #test() test logic} and the
	 * {@link #checkExceptionExpectations(Exception) checking} of the
	 * resulting (expected) {@link java.lang.Exception}.
	 * @see #test() 
	 * @see #doFail()
	 * @see #checkExceptionExpectations(Exception)
	 */
	public void runTest() {
		try {
			test();
			doFail();
		}
		catch (Exception actualException) {
			this.actualException = actualException;
			checkExceptionExpectations(actualException);
		}
	}

	/**
	 * Template method called when the test fails; i.e. the expected
	 * {@link java.lang.Exception} is <b>not</b> thrown.
	 * <p>The default implementation simply fails the test via a call to
	 * {@link junit.framework.Assert#fail(String)}.
	 * <p>If you want to customise the failure message, consider overriding
	 * {@link #createMessageForNoExceptionThrown()}, and / or supplying an
	 * extra, contextual failure message via the appropriate constructor overload.
	 * @see #getFailureMessage()
	 */
	protected void doFail() {
		Assert.fail(createMessageForNoExceptionThrown());
	}

	/**
	 * Creates the failure message used if the test fails
	 * (i.e. the expected exception is not thrown in the body of the test).
	 * @return the failure message used if the test fails
	 * @see #getFailureMessage()
	 */
	protected String createMessageForNoExceptionThrown() {
		StringBuffer sb = new StringBuffer();
		sb.append("Should have thrown a [").append(this.getExpectedException()).append("]");
		if (getFailureMessage() != null) {
			sb.append(": ").append(getFailureMessage());
		}
		return sb.toString();
	}

	/**
	 * Does the donkey work of checking (verifying) that the
	 * {@link java.lang.Exception} that was thrown in the body of a test is
	 * an instance of the {@link #getExpectedException()} class (or an
	 * instance of a subclass).
	 * <p>If you want to customise the failure message, consider overriding
	 * {@link #createMessageForWrongThrownExceptionType(Exception)}.
	 * @param actualException the {@link java.lang.Exception} that has been thrown
	 * in the body of a test method (will never be <code>null</code>)
	 */
	protected void checkExceptionExpectations(Exception actualException) {
		if (!getExpectedException().isAssignableFrom(actualException.getClass())) {
			AssertionFailedError error =
					new AssertionFailedError(createMessageForWrongThrownExceptionType(actualException));
			error.initCause(actualException);
			throw error;
		}
	}

	/**
	 * Creates the failure message used if the wrong type
	 * of {@link java.lang.Exception} is thrown in the body of the test.
	 * @param actualException the actual exception thrown
	 * @return the message for the given exception
	 */
	protected String createMessageForWrongThrownExceptionType(Exception actualException) {
		StringBuffer sb = new StringBuffer();
		sb.append("Was expecting a [").append(getExpectedException().getName());
		sb.append("] to be thrown, but instead a [").append(actualException.getClass().getName());
		sb.append("] was thrown.");
		return sb.toString();
	}


	/**
	 * Expose the actual exception thrown from {@link #test}, if any.
	 * @return the actual exception, or <code>null</code> if none
	 */
	public final Exception getActualException() {
		return this.actualException;
	}

}
