/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.isis.core.unittestsupport.jmocking; import junit.framework.AssertionFailedError; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.List; import org.jmock.Expectations; import org.jmock.auto.Mock; import org.jmock.integration.junit4.JUnit4Mockery; import org.jmock.internal.AllDeclaredFields; import org.jmock.lib.concurrent.Synchroniser; import org.junit.rules.MethodRule; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import org.picocontainer.MutablePicoContainer; import org.picocontainer.PicoBuilder; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; /** * Use as a <tt>@Rule</tt>, meaning that the <tt>@RunWith(JMock.class)</tt> can * be ignored. * * <pre> * public class MyTest { * * @Rule * public final Junit4Mockery2 context = Junit4Mockery2.createFor(Mode.INTERFACES); * * } * </pre> * * <p> * The class also adds some convenience methods, and uses a factory method to * make it explicit whether the context can mock only interfaces or interfaces * and classes. */ public class JUnitRuleMockery2 extends JUnit4Mockery implements MethodRule { /** * Factory method. */ public static JUnitRuleMockery2 createFor(final Mode mode) { final JUnitRuleMockery2 jUnitRuleMockery2 = new JUnitRuleMockery2(); if (mode == Mode.INTERFACES_AND_CLASSES) { jUnitRuleMockery2.setImposteriser(JavassistImposteriser.INSTANCE); } jUnitRuleMockery2.setThreadingPolicy(new Synchroniser()); return jUnitRuleMockery2; } /** * Annotate the field that references the class under test; * is automatically instantiated and autowired by this class, * accessible to the test using {@link JUnitRuleMockery2#getClassUnderTest()}. */ @Retention(RUNTIME) @Target(FIELD) public static @interface ClassUnderTest {} /** * Annotate fields annotated with {@link Mock}, to indicate that they should be set up * with an {@link Expectations#ignoring(Object)} expectation. */ @Retention(RUNTIME) @Target(FIELD) public static @interface Ignoring {} /** * Annotate fields annotated with {@link Mock}, to indicate that they should be set up * with an {@link Expectations#allowing(Object)} expectation. */ @Retention(RUNTIME) @Target(FIELD) public static @interface Allowing {} /** * Annotate fields annotated with {@link Mock}, to indicate that they should be set up * with an {@link Expectations#never(Object)} expectation. */ @Retention(RUNTIME) @Target(FIELD) public static @interface Never {} /** * Annotate fields annotated with {@link Mock}, to indicate that they should be set up * with an {@link Expectations#one(Object)} expectation. */ @Retention(RUNTIME) @Target(FIELD) public static @interface One {} /** * Annotate fields annotated with {@link Mock}, to indicate that they should be set up * to check the specified {@link ExpectationsOn expectation}. */ @Retention(RUNTIME) @Target(FIELD) public static @interface Checking { Class<? extends ExpectationsOn> value() default ExpectationsOn.class; } public static enum Mode { INTERFACES_ONLY, INTERFACES_AND_CLASSES; } private final MyMockomatic mockomatic = new MyMockomatic(this); private final MutablePicoContainer container = new PicoBuilder().withConstructorInjection().withSetterInjection().build(); private Class<?> cutType; private JUnitRuleMockery2() { } public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { return new Statement() { @Override public void evaluate() throws Throwable { prepare(target); base.evaluate(); assertIsSatisfied(); } private void prepare(final Object target) throws IllegalAccessException { final List<Field> allFields = AllDeclaredFields.in(target.getClass()); assertOnlyOneJMockContextIn(allFields); List<Object> mocks = fillInAutoMocks(target, allFields); Field cutField = locateClassUnderTestFieldIfAny(allFields); if (cutField != null) { cutType = cutField.getType(); for (Object mock : mocks) { container.addComponent(mock); } container.addComponent(cutType); final Object cut = container.getComponent(cutType); cutField.setAccessible(true); cutField.set(target, cut); } else { cutType = null; } } private void assertOnlyOneJMockContextIn(final List<Field> allFields) { Field contextField = null; for (final Field field : allFields) { if (JUnitRuleMockery2.class.isAssignableFrom(field.getType())) { if (null != contextField) { fail("Test class should only have one JUnitRuleMockery2 field, found " + contextField.getName() + " and " + field.getName()); } contextField = field; } } } protected Field locateClassUnderTestFieldIfAny(final List<Field> allFields) { Field cutField = null; for (final Field field : allFields) { if(field.getAnnotation(ClassUnderTest.class) != null) { if (null != cutField) { fail("Test class should only have one field annotated with @ClassUnderTest, found " + cutField.getName() + " and " + field.getName()); } cutField = field; } } return cutField; } private List<Object> fillInAutoMocks(final Object target, final List<Field> allFields) { return mockomatic.fillIn(target, allFields); } }; } public <T> T getClassUnderTest() { if(cutType == null) { throw new IllegalStateException("No field annotated @ClassUnderTest was found"); } return (T) container.getComponent(cutType); } /** * Ignoring any interaction with the mock; an allowing/ignoring mock will be * returned in turn. */ public <T> T ignoring(final T mock) { checking(new Expectations() { { ignoring(mock); } }); return mock; } /** * Allow any interaction with the mock; an allowing mock will be returned in * turn. */ public <T> T allowing(final T mock) { checking(new Expectations() { { allowing(mock); } }); return mock; } /** * Prohibit any interaction with the mock. */ public <T> T never(final T mock) { checking(new Expectations() { { never(mock); } }); return mock; } /** * Ignore a set of mocks. */ public void ignoring(Object... mocks) { for (Object mock : mocks) { ignoring(mock); } } /** * Require one interaction * @return */ public Object one(final Object mock) { checking(new Expectations() { { oneOf(mock); } }); return mock; } public static class ExpectationsOn<T> extends Expectations { public ExpectationsOn(Object mock) { this.mockObj = (T) mock; } private T mockObj; public T mock() { return mockObj; } } public <T> T checking(T mock, Class<? extends ExpectationsOn<T>> expectationsClass) { try { Constructor<? extends ExpectationsOn<T>> constructor = expectationsClass.getConstructor(Object.class); ExpectationsOn<T> expectations = constructor.newInstance(mock); checking(expectations); return mock; } catch (Exception e) { throw new AssertionFailedError("Unable to instantiate expectations class '" + expectationsClass.getName() + "'"); } } }