package org.testory.facade; import static java.util.Objects.deepEquals; import static org.testory.TestoryAssertionError.assertionError; import static org.testory.common.Effect.returned; import static org.testory.common.Effect.returnedVoid; import static org.testory.common.Effect.thrown; import static org.testory.common.Matchers.asMatcher; import static org.testory.common.Matchers.isMatcher; import static org.testory.common.Throwables.gently; import static org.testory.common.Throwables.printStackTrace; import static org.testory.plumbing.Checker.checker; import static org.testory.plumbing.CheckingProxer.checkingProxer; import static org.testory.plumbing.Inspecting.inspecting; import static org.testory.plumbing.QuietFormatter.quietFormatter; import static org.testory.plumbing.Stubbing.stubbing; import static org.testory.plumbing.VerifyingInOrder.verifyInOrder; import static org.testory.plumbing.history.FilteredHistory.filter; import static org.testory.plumbing.im.wildcard.Repairer.repairer; import static org.testory.plumbing.im.wildcard.Tokenizer.tokenizer; import static org.testory.plumbing.im.wildcard.WildcardMatcherizer.wildcardMatcherizer; import static org.testory.plumbing.im.wildcard.WildcardSupport.wildcardSupport; import static org.testory.plumbing.inject.ArrayMaker.singletonArray; import static org.testory.plumbing.inject.ChainedMaker.chain; import static org.testory.plumbing.inject.FinalMaker.finalMaker; import static org.testory.plumbing.inject.RandomPrimitiveMaker.randomPrimitiveMaker; import static org.testory.plumbing.mock.NiceMockMaker.nice; import static org.testory.plumbing.mock.RawMockMaker.rawMockMaker; import static org.testory.plumbing.mock.SaneMockMaker.sane; import static org.testory.plumbing.mock.UniqueNamer.uniqueNamer; import static org.testory.proxy.Invocation.invocation; import static org.testory.proxy.Typing.subclassing; import static org.testory.proxy.handler.DelegatingHandler.delegatingTo; import static org.testory.proxy.handler.ReturningDefaultValueHandler.returningDefaultValue; import static org.testory.proxy.proxer.NonFinalProxer.nonFinal; import static org.testory.proxy.proxer.TypeSafeProxer.typeSafe; import static org.testory.proxy.proxer.WrappingProxer.wrapping; import org.testory.TestoryException; import org.testory.common.Closure; import org.testory.common.DiagnosticMatcher; import org.testory.common.Effect; import org.testory.common.Effect.Returned; import org.testory.common.Effect.ReturnedObject; import org.testory.common.Effect.Thrown; import org.testory.common.Formatter; import org.testory.common.Matcher; import org.testory.common.Nullable; import org.testory.common.Optional; import org.testory.common.VoidClosure; import org.testory.plumbing.Checker; import org.testory.plumbing.Inspecting; import org.testory.plumbing.Maker; import org.testory.plumbing.QuietFormatter; import org.testory.plumbing.VerifyingInOrder; import org.testory.plumbing.history.FilteredHistory; import org.testory.plumbing.history.History; import org.testory.plumbing.im.Matcherizer; import org.testory.plumbing.im.wildcard.WildcardException; import org.testory.plumbing.im.wildcard.WildcardSupport; import org.testory.plumbing.inject.Injector; import org.testory.plumbing.mock.Namer; import org.testory.proxy.Handler; import org.testory.proxy.Invocation; import org.testory.proxy.InvocationMatcher; import org.testory.proxy.Proxer; import org.testory.proxy.proxer.CglibProxer; public class DefaultFacade implements Facade { private final History history; private final FilteredHistory<Inspecting> inspectingHistory; private final FilteredHistory<Invocation> invocationHistory; private final Formatter formatter; private final Checker checker; private final Proxer proxer; private final Namer mockNamer; private final Maker mockMaker; private final Injector injector; private final WildcardSupport wildcardSupport; private final Matcherizer matcherizer; private DefaultFacade(History mutableHistory) { Class<TestoryException> exception = TestoryException.class; QuietFormatter quietFormatter = quietFormatter(); formatter = quietFormatter; history = quietFormatter.quiet(mutableHistory); inspectingHistory = filter(Inspecting.class, history); invocationHistory = filter(Invocation.class, history); checker = checker(history, exception); proxer = wrapping(exception, nonFinal(typeSafe(wrapping(exception, new CglibProxer())))); mockNamer = uniqueNamer(history); mockMaker = mockMaker(history, checkingProxer(checker, proxer)); injector = injector(mockMaker); wildcardSupport = wildcardSupport(history, tokenizer(), formatter); matcherizer = wildcardMatcherizer(history, repairer(), formatter); } public static DefaultFacade defaultFacade(History mutableHistory) { return new DefaultFacade(mutableHistory); } private static Maker mockMaker(History history, Proxer proxer) { Maker rawMockMaker = rawMockMaker(proxer, history); Maker niceMockMaker = nice(rawMockMaker, history); Maker saneNiceMockMaker = sane(niceMockMaker, history); return saneNiceMockMaker; } private static Injector injector(Maker mockMaker) { Maker fieldMaker = singletonArray(chain(randomPrimitiveMaker(), finalMaker(), mockMaker)); return new Injector(fieldMaker); } public void givenTest(Object test) { try { injector.inject(test); } catch (RuntimeException e) { throw new TestoryException(e); } } public void given(Closure closure) { checker.notNull(closure); try { closure.invoke(); } catch (Throwable e) { throw new TestoryException(e); } } public void given(VoidClosure closure) { checker.notNull(closure); given(asClosure(closure)); } public <T> T given(T object) { return object; } public void given(boolean primitive) {} public void given(double primitive) {} public <T> T givenTry(T object) { checker.notNull(object); Handler handler = new Handler() { public Object handle(Invocation invocation) { try { return invocation.invoke(); } catch (Throwable e) { return null; } } }; return proxyWrapping(object, handler); } public void givenTimes(int number, Closure closure) { checker.notNegative(number); checker.notNull(closure); for (int i = 0; i < number; i++) { try { closure.invoke(); } catch (Throwable throwable) { throw gently(throwable); } } } public void givenTimes(int number, VoidClosure closure) { checker.notNull(closure); givenTimes(number, asClosure(closure)); } public <T> T givenTimes(final int number, T object) { checker.notNegative(number); checker.notNull(object); Handler handler = new Handler() { public Object handle(Invocation invocation) throws Throwable { for (int i = 0; i < number; i++) { invocation.invoke(); } return null; } }; return proxyWrapping(object, handler); } public <T> T mock(Class<T> type) { checker.notNull(type); String name = mockNamer.name(type); return mockMaker.make(type, name); } public <T> T spy(T real) { checker.notNull(real); Class<T> type = (Class<T>) real.getClass(); T mock = mock(type); given(willSpy(real), onInstance(mock)); return mock; } public <T> T given(final Handler handler, T mock) { checker.notNull(handler); checker.mock(mock); return proxyWrapping(mock, new Handler() { public Object handle(Invocation invocation) { history.add(stubbing(matcherize(invocation), handler)); return null; } }); } public void given(Handler handler, InvocationMatcher invocationMatcher) { checker.notNull(handler); checker.notNull(invocationMatcher); history.add(stubbing(invocationMatcher, handler)); } public Handler willReturn(@Nullable final Object object) { return new Handler() { public Object handle(Invocation invocation) { return object; } }; } public Handler willThrow(final Throwable throwable) { checker.notNull(throwable); return new Handler() { public Object handle(Invocation invocation) throws Throwable { throw throwable.fillInStackTrace(); } }; } public Handler willRethrow(final Throwable throwable) { checker.notNull(throwable); return new Handler() { public Object handle(Invocation invocation) throws Throwable { throw throwable; } }; } public Handler willSpy(final Object real) { checker.notNull(real); return new Handler() { public Object handle(Invocation invocation) throws Throwable { return invocation(invocation.method, real, invocation.arguments).invoke(); } }; } public <T> T any(Class<T> type) { checker.notNull(type); return (T) wildcardSupport.any(type); } public <T> T any(Class<T> type, Object matcher) { checker.matcher(matcher); return (T) wildcardSupport.any(type, matcher); } public <T> T anyInstanceOf(Class<T> type) { checker.notNull(type); return (T) wildcardSupport.anyInstanceOf(type); } public boolean a(boolean value) { return a((Boolean) value); } public char a(char value) { return a((Character) value); } public byte a(byte value) { return a((Byte) value); } public short a(short value) { return a((Short) value); } public int a(int value) { return a((Integer) value); } public long a(long value) { return a((Long) value); } public float a(float value) { return a((Float) value); } public double a(double value) { return a((Double) value); } public <T> T a(T value) { checker.notNull(value); return (T) wildcardSupport.a(value); } public <T> T the(T value) { checker.notNull(value); return (T) wildcardSupport.the(value); } public void the(boolean value) { throw new TestoryException(); } public void the(double value) { throw new TestoryException(); } public InvocationMatcher onInstance(final Object mock) { checker.mock(mock); return new InvocationMatcher() { public boolean matches(Invocation invocation) { return invocation.instance == mock; } public String toString() { return "onInstance(" + mock + ")"; } }; } public InvocationMatcher onReturn(final Class<?> type) { checker.notNull(type); return new InvocationMatcher() { public boolean matches(Invocation invocation) { return type == invocation.method.getReturnType(); } public String toString() { return "onReturn(" + type.getName() + ")"; } }; } public InvocationMatcher onRequest(final Class<?> type, final Object... arguments) { checker.notNull(type); checker.notNull(arguments); return new InvocationMatcher() { public boolean matches(Invocation invocation) { return type == invocation.method.getReturnType() && deepEquals(arguments, invocation.arguments.toArray()); } public String toString() { StringBuilder builder = new StringBuilder(); builder.append("onRequest(").append(type.getName()); for (Object argument : arguments) { builder.append(", ").append(argument); } builder.append(")"); return builder.toString(); } }; } public <T> T when(T object) { history.add(inspecting(returned(object))); try { return proxyWrapping(object, new Handler() { public Object handle(Invocation invocation) { history.add(inspecting(effectOf(invocation))); return null; } }); } catch (RuntimeException e) { return null; } } private static Effect effectOf(Invocation invocation) { Object object; try { object = invocation.invoke(); } catch (Throwable throwable) { return thrown(throwable); } return invocation.method.getReturnType() == void.class ? returnedVoid() : returned(object); } public void when(Closure closure) { checker.notNull(closure); history.add(inspecting(effectOf(closure))); } private static Effect effectOf(Closure closure) { Object object; try { object = closure.invoke(); } catch (Throwable throwable) { return thrown(throwable); } return returned(object); } public void when(VoidClosure closure) { checker.notNull(closure); history.add(inspecting(effectOf(closure))); } private static Effect effectOf(VoidClosure closure) { try { closure.invoke(); } catch (Throwable throwable) { return thrown(throwable); } return returnedVoid(); } public void when(boolean value) { when((Object) value); } public void when(char value) { when((Object) value); } public void when(byte value) { when((Object) value); } public void when(short value) { when((Object) value); } public void when(int value) { when((Object) value); } public void when(long value) { when((Object) value); } public void when(float value) { when((Object) value); } public void when(double value) { when((Object) value); } public void thenReturned(@Nullable Object objectOrMatcher) { Effect effect = getLastEffect(); boolean expected = effect instanceof ReturnedObject && (deepEquals(objectOrMatcher, ((ReturnedObject) effect).object) || objectOrMatcher != null && isMatcher(objectOrMatcher) && asMatcher(objectOrMatcher).matches(((ReturnedObject) effect).object)); if (!expected) { String diagnosis = objectOrMatcher != null && isMatcher(objectOrMatcher) && effect instanceof ReturnedObject ? tryFormatDiagnosis(objectOrMatcher, ((ReturnedObject) effect).object) : ""; throw assertionError("\n" + formatSection("expected returned", objectOrMatcher) + formatBut(effect) + diagnosis); } } public void thenReturned(boolean value) { thenReturned((Object) value); } public void thenReturned(char value) { thenReturned((Object) value); } public void thenReturned(byte value) { thenReturned((Object) value); } public void thenReturned(short value) { thenReturned((Object) value); } public void thenReturned(int value) { thenReturned((Object) value); } public void thenReturned(long value) { thenReturned((Object) value); } public void thenReturned(float value) { thenReturned((Object) value); } public void thenReturned(double value) { thenReturned((Object) value); } public void thenReturned() { Effect effect = getLastEffect(); boolean expected = effect instanceof Returned; if (!expected) { throw assertionError("\n" + formatSection("expected returned", "") + formatBut(effect)); } } public void thenThrown(Object matcher) { checker.matcher(matcher); Effect effect = getLastEffect(); boolean expected = effect instanceof Thrown && asMatcher(matcher).matches(((Thrown) effect).throwable); if (!expected) { String diagnosis = effect instanceof Thrown ? tryFormatDiagnosis(matcher, ((Thrown) effect).throwable) : ""; throw assertionError("\n" + formatSection("expected thrown", matcher) + formatBut(effect) + diagnosis); } } public void thenThrown(Throwable throwable) { checker.notNull(throwable); Effect effect = getLastEffect(); boolean expected = effect instanceof Thrown && deepEquals(throwable, ((Thrown) effect).throwable); if (!expected) { throw assertionError("\n" + formatSection("expected thrown", throwable) + formatBut(effect)); } } public void thenThrown(Class<? extends Throwable> type) { checker.notNull(type); Effect effect = getLastEffect(); boolean expected = effect instanceof Thrown && type.isInstance(((Thrown) effect).throwable); if (!expected) { throw assertionError("\n" + formatSection("expected thrown", type.getName()) + formatBut(effect)); } } public void thenThrown() { Effect effect = getLastEffect(); boolean expected = effect instanceof Thrown; if (!expected) { throw assertionError("\n" + formatSection("expected thrown", "") + formatBut(effect)); } } public void then(boolean condition) { if (!condition) { throw assertionError("\n" + formatSection("expected", "true") + formatSection("but was", "false")); } } public void then(@Nullable Object object, Object matcher) { checker.matcher(matcher); if (!asMatcher(matcher).matches(object)) { throw assertionError("\n" + formatSection("expected", matcher) + formatSection("but was", object) + tryFormatDiagnosis(matcher, object)); } } public void thenEqual(@Nullable Object object, @Nullable Object expected) { if (!deepEquals(object, expected)) { throw assertionError("\n" + formatSection("expected", expected) + formatSection("but was", object)); } } public <T> T thenCalled(T mock) { checker.mock(mock); return thenCalledTimes(exactly(1), mock); } public void thenCalled(InvocationMatcher invocationMatcher) { checker.notNull(invocationMatcher); thenCalledTimes(exactly(1), invocationMatcher); } public <T> T thenCalledNever(T mock) { checker.mock(mock); return thenCalledTimes(exactly(0), mock); } public void thenCalledNever(InvocationMatcher invocationMatcher) { checker.notNull(invocationMatcher); thenCalledTimes(exactly(0), invocationMatcher); } public <T> T thenCalledTimes(int number, T mock) { checker.notNegative(number); checker.mock(mock); return thenCalledTimes(exactly(number), mock); } public void thenCalledTimes(int number, InvocationMatcher invocationMatcher) { checker.notNegative(number); checker.notNull(invocationMatcher); thenCalledTimes(exactly(number), invocationMatcher); } public <T> T thenCalledTimes(final Object numberMatcher, T mock) { checker.matcher(numberMatcher); checker.mock(mock); Handler handler = new Handler() { public Object handle(Invocation invocation) { thenCalledTimes(numberMatcher, matcherize(invocation)); return null; } }; return proxyWrapping(mock, handler); } public void thenCalledTimes(Object numberMatcher, InvocationMatcher invocationMatcher) { checker.matcher(numberMatcher); checker.notNull(invocationMatcher); int numberOfCalls = 0; for (Invocation invocation : invocationHistory.get()) { if (invocationMatcher.matches(invocation)) { numberOfCalls++; } } boolean expected = asMatcher(numberMatcher).matches(numberOfCalls); if (!expected) { throw assertionError("\n" + formatSection("expected called times " + numberMatcher, invocationMatcher) + formatSection("but called", "times " + numberOfCalls) + formatInvocations()); } } public <T> T thenCalledInOrder(T mock) { checker.mock(mock); Handler handler = new Handler() { public Object handle(Invocation invocation) { thenCalledInOrder(matcherize(invocation)); return null; } }; return proxyWrapping(mock, handler); } public void thenCalledInOrder(InvocationMatcher invocationMatcher) { checker.notNull(invocationMatcher); Optional<VerifyingInOrder> verified = verifyInOrder(invocationMatcher, history.get()); if (verified.isPresent()) { history.add(verified.get()); } else { throw assertionError("\n" + formatSection("expected called in order", invocationMatcher) + " but not called\n" + formatInvocations()); } } private InvocationMatcher matcherize(Invocation invocation) { try { return matcherizer.matcherize(invocation); } catch (WildcardException e) { throw new TestoryException(e); } } private Effect getLastEffect() { checker.mustCallWhen(); return inspectingHistory.get().get().effect; } private String formatSection(String caption, @Nullable Object content) { return "" + " " + caption + "\n" + " " + formatter.format(content) + "\n"; } private String formatBut(Effect effect) { return effect instanceof Returned ? effect instanceof ReturnedObject ? formatSection("but returned", ((ReturnedObject) effect).object) : formatSection("but returned", "void") : "" + formatSection("but thrown", ((Thrown) effect).throwable) + "\n" + printStackTrace(((Thrown) effect).throwable); } private String formatInvocations() { StringBuilder builder = new StringBuilder(); for (Object event : history.get().reverse()) { if (event instanceof Invocation) { Invocation invocation = (Invocation) event; builder.append(" ").append(formatter.format(invocation)).append("\n"); } } if (builder.length() > 0) { builder.insert(0, " actual invocations\n"); } else { builder.insert(0, " actual invocations\n none\n"); } return builder.toString(); } private String tryFormatDiagnosis(Object matcher, Object item) { Matcher asMatcher = asMatcher(matcher); return asMatcher instanceof DiagnosticMatcher ? formatSection("diagnosis", ((DiagnosticMatcher) asMatcher).diagnose(item)) : ""; } private static Closure asClosure(final VoidClosure closure) { return new Closure() { public Object invoke() throws Throwable { closure.invoke(); return null; } }; } private static Matcher exactly(final int number) { return new Matcher() { public boolean matches(Object item) { return item.equals(number); } public String toString() { return "" + number; } }; } private <T> T proxyWrapping(T wrapped, Handler handler) { return (T) proxer.proxy( subclassing(wrapped.getClass()), returningDefaultValue(delegatingTo(wrapped, handler))); } }