/* * Copyright 2006 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 jdave; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import jdave.containment.MapContainment; import jdave.equality.DeltaEqualityCheck; import jdave.equality.EqualsEqualityCheck; import jdave.equality.LongEqualsEqualityCheck; import jdave.equality.NotEqualsEqualityCheck; import jdave.mock.MockSupport; import jdave.util.Collections; import jdave.util.Primitives; import org.hamcrest.Matcher; import org.hamcrest.StringDescription; /** * @author Joni Freeman * @author Pekka Enberg */ public abstract class Specification<T> extends MockSupport { protected Specification<T> should = this; protected Specification<T> does = this; protected Specification<T> must = this; private boolean actualState = true; public T be; public T context; private final List<ILifecycleListener> listeners = new ArrayList<ILifecycleListener>(); private IContextObjectFactory<T> contextObjectFactory = new DefaultContextObjectFactory<T>(); private static IStringComparisonFailure stringComparisonFailure = new ExpectationFailedStringComparisonFailure(); public Not<T> not() { actualState = false; return new Not<T>(this); } public void specify(final boolean expected) { specify0(null, expected); } private void specify0(final T actual, final boolean expected) { try { if (expected != actualState) { throw new ExpectationFailedException("Expected: " + "true" + ", but was: " + "false" + (actual != null ? " (actual was '" + actual + "')" : "")); } } finally { resetActualState(); } } public void specify(final T actual, final boolean expected) { specify0(actual, expected); } public void specify(final T actual, final Boolean expected) { specify0(actual, expected); } private void resetActualState() { actualState = true; } public <E> void specify(final Iterator<E> actual, final IContainment<E> containment) { specify(Collections.<E> list(actual), containment); } public <E> void specify(final Collection<E> actual, final IContainment<E> containment) { try { if (!containment.matches(actual)) { throw new ExpectationFailedException(containment.error(actual)); } } finally { resetActualState(); } } public <E> void specify(final Object[] actual, final IContainment<E> containment) { specify(Arrays.asList(actual), containment); } public <E> void specify(final boolean[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final byte[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final char[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final double[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final float[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final int[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final long[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public <E> void specify(final short[] actual, final IContainment<E> containment) { specify(Primitives.asList(actual), containment); } public void specify(final Object actual, final Object expected) { final IEqualityCheck equalityCheck = new EqualsEqualityCheck(expected); try { if (!equalityCheck.matches(actual)) { throw new ExpectationFailedException(equalityCheck.error(actual)); } } finally { resetActualState(); } } public void specify(final Object actual, final IEqualityCheck equalityCheck) { try { equalityCheck.verify(actual); } finally { resetActualState(); } } /** * Matches the actual object using Hamcrest Matcher. * <p> * Hamcrest provides a library of matcher objects allowing 'match' rules * to be defined declaratively. <blockquote> * * <pre> * <code> * import static org.hamcrest.Matchers.*; * public class HamcrestSampleSpec extends Specification<Person> { * public class SampleContext { * public void sample() { * specify(person.getAge(), greaterThan(30)); * } * } * } * </code> * </pre> * * </blockquote> * <p> * See <a href="http://code.google.com/p/hamcrest/">Hamcrest home page</a> */ public void specify(final Object actual, final Matcher<?> matcher) { try { if (!matcher.matches(actual)) { throw new ExpectationFailedException(actual + " " + StringDescription.toString(matcher)); } } finally { resetActualState(); } } /** * Matches all the actual objects using Hamcrest Matcher. * <p> * Hamcrest provides a library of matcher objects allowing 'match' rules * to be defined declaratively. <blockquote> * * <pre> * <code> * import static org.hamcrest.Matchers.*; * public class HamcrestSampleSpec extends Specification<Person> { * public class SampleContext { * public void sample() { * specify(persons, where(new Each<Person>() {{ matches(item.getAge(), is(greaterThan(30))); }})); * } * } * } * </code> * </pre> * * </blockquote> * <p> * See <a href="http://code.google.com/p/hamcrest/">Hamcrest home page</a> */ public <E> void specify(final Collection<E> actual, final Where<E> where) { specify(actual.iterator(), where); } /** * @see #specify(Collection, Where) */ public <E> void specify(final Iterable<E> actual, final Where<E> where) { specify(actual.iterator(), where); } /** * @see #specify(Collection, Where) */ public <E> void specify(final Object[] actual, final Where<E> where) { specify(Arrays.asList(actual), where); } /** * @see #specify(Collection, Where) */ public <E> void specify(final Iterator<E> actual, final Where<E> where) { try { int index = 0; while (actual.hasNext()) { where.match(actual.next(), index); index++; } where.areAllMatchersUsed(index); } finally { resetActualState(); } } public <E> Where<E> where(final Each<E> each) { return new Where<E>(each); } /** * The given block is expected to throw an exception. * <p> * There's two variants for setting exception expectations. <blockquote> * * <pre> * <code> * specify(new Block() { ... }, should.raise(SomeException.class); * specify(new Block() { ... }, should.raiseExactly(SomeException.class); * </code> * </pre> * * </blockquote> The first one accepts the given exception or any of its * subclasses, the second one expects an exact exception type. Both can * additionally be checked against expected exception message: * <blockquote> * * <pre> * <code> * specify(new Block() { ... }, should.raise(SomeException.class, "expected message"); * specify(new Block() { ... }, should.raiseExactly(SomeException.class, "expected message"); * </code> * </pre> * * </blockquote> */ public <V extends Throwable> void specify(final Block block, final ExpectedException<V> expectation) { try { specifyThrow(block, expectation); } finally { resetActualState(); } } private void specifyThrow(final Block block, final ExpectedException<? extends Throwable> expectation) { try { block.run(); } catch (final Throwable t) { if (!expectation.matches(t)) { throw new ExpectationFailedException(expectation.error(t), t); } return; } throw new ExpectationFailedException(expectation.notThrown()); } /** * The given block is expected to not throw an exception. * <p> * There's two variants for setting exception expectations. <blockquote> * * <pre> * <code> * specify(new Block() { ... }, should.not().raise(SomeException.class); * specify(new Block() { ... }, should.not().raiseExactly(SomeException.class); * </code> * </pre> * * </blockquote> The first one expects that the given block does not throw * the exception or any of its subclasses, the second one expects that the * given block does not throw the given exact exception type. */ public <V extends Throwable> void specify(final Block block, final ExpectedNoThrow<V> expectation) throws Throwable { try { specifyNoThrow(block, expectation); } finally { resetActualState(); } } private void specifyNoThrow(final Block block, final ExpectedNoThrow<? extends Throwable> expectation) throws Throwable { try { block.run(); } catch (final Throwable t) { if (!expectation.matches(t)) { throw new ExpectationFailedException(expectation.error(t), t); } throw t; } } public void specify(final Object obj, final IContract contract) { contract.isSatisfied(obj); } public IEqualityCheck equal(final long expected) { return new LongEqualsEqualityCheck(expected); } public IEqualityCheck equal(final String obj) { if (obj == null) { return new EqualsEqualityCheck(obj); } return new StringEqualsEqualityCheck(this, obj); } public IEqualityCheck equal(final Object obj) { return new EqualsEqualityCheck(obj); } public IEqualityCheck equal(final Number expectedNumber, final double delta) { return new DeltaEqualityCheck(expectedNumber, delta); } /** * @see #specify(Block, ExpectedException) * @see #specify(Block, ExpectedNoThrow) */ public <E extends Throwable> ExpectedException<E> raise(final Class<E> expected) { return new ExpectedException<E>(expected); } /** * @see #specify(Block, ExpectedException) * @see #specify(Block, ExpectedNoThrow) */ public <E extends Throwable> ExpectedException<E> raise(final Class<E> expectedType, final String expectedMessage) { return new ExpectedExceptionWithMessage<E>(expectedType, expectedMessage); } /** * @see #specify(Block, ExpectedException) * @see #specify(Block, ExpectedNoThrow) */ public <E extends Throwable> ExpectedException<E> raiseExactly(final Class<E> expected) { return new ExactExpectedException<E>(expected); } /** * @see #specify(Block, ExpectedException) * @see #specify(Block, ExpectedNoThrow) */ public <E extends Throwable> ExpectedException<E> raiseExactly(final Class<E> expected, final String expectedMessage) { return new ExactExpectedExceptionWithMessage<E>(expected, expectedMessage); } public IContract satisfies(final IContract contract) { return contract; } /** * Create this specification. * <p> * This method is called before the <code>create</code> method of the * executed context has been called. Override this method to add common * initialization code for contexts within a specification. */ public void create() throws Exception { } /** * Destroy this specification. * <p> * This method is called after the optional <code>destroy</code> method of * the excuted context has been called. Override this method to add common * destroy code for contexts within a specification. */ public void destroy() throws Exception { } /** * Returns <code>true</code> if thread local isolation is needed for this * specification. * <p> * Some contexts set thread local variables. This may cause following * behaviors to fail if they depend on initial thread local state. Thread * locals can be isolated for all behavior methods of current * specification by overiding this method and returning <code>true</code>. * Then a new fresh thread is created for all methods in the * specification. * * @return <code>true</code> if thread local isolation is needed for this * specification. The default is <code>false</code>. */ public boolean needsThreadLocalIsolation() { return false; } protected void setContextObjectFactory(final IContextObjectFactory<T> contextObjectFactory) { this.contextObjectFactory = contextObjectFactory; } public IContextObjectFactory<T> getContextObjectFactory() { return contextObjectFactory; } /** * Add a <code>ILifecycleListener</code> listener to specification. * <p> * ILifecycleListener will be notified when contexts are instantiated and * context objects are created and destroyed. * * @param listener a listener to get lifecycle event notifications */ protected void addListener(final ILifecycleListener listener) { listeners.add(listener); } public void fireAfterContextInstantiation(final Object contextInstance) { for (final ILifecycleListener listener : listeners) { listener.afterContextInstantiation(contextInstance); } } public void fireAfterContextCreation(final Object contextInstance, final Object createdContext) { for (final ILifecycleListener listener : listeners) { listener.afterContextCreation(contextInstance, createdContext); } } public void fireAfterContextDestroy(final Object contextInstance) { for (final ILifecycleListener listener : listeners) { listener.afterContextDestroy(contextInstance); } } public IEqualityCheck isNotNull() { return new NotEqualsEqualityCheck(null) { @Override public String error(final Object actual) { return "Expected a non-null value"; } }; } public static void setStringComparisonFailure(final IStringComparisonFailure failure) { stringComparisonFailure = failure; } IStringComparisonFailure stringComparisonFailure() { return stringComparisonFailure; } public MapContainment maps(final Object... keys) { return new MapContainment(keys); } public void specify(final Map<?, ?> actual, final MapContainment containment) { containment.verify(actual); } /** * Fail with a message. * * @param message a message printed to the console */ public void fail(final String message) { throw new ExpectationFailedException(message); } }