/* * * * Copyright 2010, Unitils.org * * * * 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.unitils.mock.core; import org.unitils.core.util.ObjectToInjectHolder; import org.unitils.mock.Mock; import org.unitils.mock.annotation.MatchStatement; import org.unitils.mock.argumentmatcher.ArgumentMatcherRepository; import org.unitils.mock.core.matching.MatchingInvocationBuilder; import org.unitils.mock.core.matching.MatchingInvocationHandler; import org.unitils.mock.core.matching.impl.AssertInvokedInSequenceVerifyingMatchingInvocationHandler; import org.unitils.mock.core.matching.impl.AssertInvokedVerifyingMatchingInvocationHandler; import org.unitils.mock.core.matching.impl.AssertNotInvokedVerifyingMatchingInvocationHandler; import org.unitils.mock.core.matching.impl.BehaviorDefiningMatchingInvocationHandler; import org.unitils.mock.mockbehavior.MockBehavior; import org.unitils.mock.mockbehavior.impl.ExceptionThrowingMockBehavior; import org.unitils.mock.mockbehavior.impl.ValueReturningMockBehavior; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.uncapitalize; import static org.unitils.mock.core.proxy.ProxyFactory.createInitializedOrUninitializedInstanceOfType; import static org.unitils.util.ReflectionUtils.getGenericType; /** * Implementation of a Mock. * * @author Filip Neven * @author Tim Ducheyne * @author Kenny Claes */ public class MockObject<T> implements Mock<T>, MockFactory, ObjectToInjectHolder<T> { /* The name of the mock (e.g. the name of the field) */ protected String name; /* The class type that is mocked */ protected Class<T> mockedType; protected MockProxy<T> mockProxy; /* Mock behaviors that are removed once they have been matched */ protected BehaviorDefiningInvocations oneTimeMatchingBehaviorDefiningInvocations; /* Mock behaviors that can be matched and re-used for several invocation */ protected BehaviorDefiningInvocations alwaysMatchingBehaviorDefiningInvocations; /* Created chained mocks per mock name */ protected Map<String, Mock<?>> chainedMocksPerName; /* The scenario that will record all observed invocations */ protected static ThreadLocal<Scenario> scenarioThreadLocal = new ThreadLocal<Scenario>(); protected static ThreadLocal<MatchingInvocationBuilder> matchingInvocationBuilderThreadLocal = new ThreadLocal<MatchingInvocationBuilder>(); public static Scenario getCurrentScenario() { return scenarioThreadLocal.get(); } /** * Creates a mock of the given type with un-capitalized type name + Mock as name, e.g. myServiceMock. * * There is no .class literal for generic types. Therefore you need to pass the raw type when mocking generic types. * E.g. Mock<List<String>> myMock = new MockObject("myMock", List.class, this); * * If the mocked type does not correspond to the declared type, a ClassCastException will occur when the mock * is used. * * @param mockedType The mock type that will be proxied, use the raw type when mocking generic types, not null * @param testObject The test object, not null */ public MockObject(Class<?> mockedType, Object testObject) { this(null, mockedType, testObject); } /** * Creates a mock of the given type. * * There is no .class literal for generic types. Therefore you need to pass the raw type when mocking generic types. * E.g. Mock<List<String>> myMock = new MockObject("myMock", List.class, this); * * If the mocked type does not correspond to the declared type, a ClassCastException will occur when the mock * is used. * * If no name is given the un-capitalized type name + Mock is used, e.g. myServiceMock * * @param name The name of the mock, e.g. the field-name, null for the default * @param mockedType The mock type that will be proxied, use the raw type when mocking generic types, not null * @param testObject The test object, not null */ @SuppressWarnings({"unchecked"}) public MockObject(String name, Class<?> mockedType, Object testObject) { if (isBlank(name)) { this.name = uncapitalize(mockedType.getSimpleName()) + "Mock"; } else { this.name = name; } this.mockedType = (Class<T>) mockedType; this.oneTimeMatchingBehaviorDefiningInvocations = createOneTimeMatchingBehaviorDefiningInvocations(); this.alwaysMatchingBehaviorDefiningInvocations = createAlwaysMatchingBehaviorDefiningInvocations(); this.chainedMocksPerName = new HashMap<String, Mock<?>>(); Scenario scenario = getScenario(testObject); if (scenario.getTestObject() != testObject) { scenario.reset(); getMatchingInvocationBuilder().reset(); scenario.setTestObject(testObject); } this.mockProxy = createMockProxy(); } // // Implementation of the ObjectToInjectHolder interface. Implementing this interface makes sure that the // proxy instance is injected instead of this object (which doesn't directly implement the mocked interface) // /** * Returns the mock proxy instance. This is the object that must be injected if the field that it holds is * annotated with @InjectInto or one of it's equivalents. * * @return The mock proxy instance, not null */ public T getObjectToInject() { return getMock(); } /** * @param field The field that declared this mock object, null if there is no field (or not known) * @return The type of the object to inject (i.e. the mocked type), not null. */ public Type getObjectToInjectType(Field field) { if (field == null) { return mockedType; } return getGenericType(field); } // // Implementation of the Mock interfaces // /** * Returns the mock proxy instance. This is the instance that can be used to perform the test. * You could for example inject it in the tested object. It will then perform the defined behavior and record * all observed method invocations so that assertions can be performed afterwards. * * @return The proxy instance, not null */ public T getMock() { return mockProxy.getProxy(); } /** * @return the type of the mock, not null */ public Class<?> getMockedType() { return mockedType; } /** * Defines behavior for this mock so that it will return the given value when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.returns("aValue").method1(); * <p/> * will return "aValue" when method1 is called. * <p/> * Note that this behavior is executed each time a match is found. So "aValue" will be returned * each time method1() is called. If you only want to return the value once, use the {@link #onceReturns} method. * * @param returnValue The value to return * @return The proxy instance that will record the method call, not null */ @MatchStatement public T returns(Object returnValue) { MatchingInvocationHandler matchingInvocationHandler = createAlwaysMatchingBehaviorDefiningMatchingInvocationHandler(new ValueReturningMockBehavior(returnValue)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that it raises the given exception when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.raises(new MyException()).method1(); * <p/> * will throw the given exception when method1 is called. * <p/> * Note that this behavior is executed each time a match is found. So the exception will be raised * each time method1() is called. If you only want to raise the exception once, use the {@link #onceRaises} method. * * @param exception The exception to raise, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T raises(Throwable exception) { MatchingInvocationHandler matchingInvocationHandler = createAlwaysMatchingBehaviorDefiningMatchingInvocationHandler(new ExceptionThrowingMockBehavior(exception)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that it raises the given exception when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.raises(MyException.class).method1(); * <p/> * will throw an instance of the given exception class when method1 is called. * <p/> * Note that this behavior is executed each time a match is found. So the exception will be raised * each time method1() is called. If you only want to raise the exception once, use the {@link #onceRaises} method. * * @param exceptionClass The type of exception to raise, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T raises(Class<? extends Throwable> exceptionClass) { Throwable exception = createInitializedOrUninitializedInstanceOfType(exceptionClass); exception.fillInStackTrace(); MatchingInvocationHandler matchingInvocationHandler = createAlwaysMatchingBehaviorDefiningMatchingInvocationHandler(new ExceptionThrowingMockBehavior(exception)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that will be performed when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.performs(new MyMockBehavior()).method1(); * <p/> * will execute the given mock behavior when method1 is called. * <p/> * Note that this behavior is executed each time a match is found. So the behavior will be executed * each time method1() is called. If you only want to execute the behavior once, use the {@link #oncePerforms} method. * * @param mockBehavior The behavior to perform, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T performs(MockBehavior mockBehavior) { MatchingInvocationHandler matchingInvocationHandler = createAlwaysMatchingBehaviorDefiningMatchingInvocationHandler(mockBehavior); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that it will return the given value when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.returns("aValue").method1(); * <p/> * will return "aValue" when method1 is called. * <p/> * Note that this behavior is executed only once. If method1() is invoked a second time, a different * behavior definition will be used (if defined) or a default value will be returned. If you want this * definition to be able to be matched multiple times, use the method {@link #returns} instead. * * @param returnValue The value to return * @return The proxy instance that will record the method call, not null */ @MatchStatement public T onceReturns(Object returnValue) { MatchingInvocationHandler matchingInvocationHandler = createOneTimeMatchingBehaviorDefiningMatchingInvocationHandler(new ValueReturningMockBehavior(returnValue)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that it raises the given exception when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.raises(new MyException()).method1(); * <p/> * will throw the given exception when method1 is called. * <p/> * Note that this behavior is executed only once. If method1() is invoked a second time, a different * behavior definition will be used (if defined) or a default value will be returned. If you want this * definition to be able to be matched multiple times, use the method {@link #raises} instead. * * @param exception The exception to raise, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T onceRaises(Throwable exception) { MatchingInvocationHandler matchingInvocationHandler = createOneTimeMatchingBehaviorDefiningMatchingInvocationHandler(new ExceptionThrowingMockBehavior(exception)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that it raises an instance of the given exception class when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.raises(new MyException()).method1(); * <p/> * will throw an instance of the given exception class when method1 is called. * <p/> * Note that this behavior is executed only once. If method1() is invoked a second time, a different * behavior definition will be used (if defined) or a default value will be returned. If you want this * definition to be able to be matched multiple times, use the method {@link #raises} instead. * * @param exceptionClass The type of exception to raise, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T onceRaises(Class<? extends Throwable> exceptionClass) { Throwable exception = createInitializedOrUninitializedInstanceOfType(exceptionClass); MatchingInvocationHandler matchingInvocationHandler = createOneTimeMatchingBehaviorDefiningMatchingInvocationHandler(new ExceptionThrowingMockBehavior(exception)); return startMatchingInvocation(matchingInvocationHandler); } /** * Defines behavior for this mock so that will be performed when the invocation following * this call matches the observed behavior. E.g. * <p/> * mock.performs(new MyMockBehavior()).method1(); * <p/> * will execute the given mock behavior when method1 is called. * <p/> * Note that this behavior is executed only once. If method1() is invoked a second time, a different * behavior definition will be used (if defined) or a default value will be returned. If you want this * definition to be able to be matched multiple times, use the method {@link #performs} instead. * * @param mockBehavior The behavior to perform, not null * @return The proxy instance that will record the method call, not null */ @MatchStatement public T oncePerforms(MockBehavior mockBehavior) { MatchingInvocationHandler matchingInvocationHandler = createOneTimeMatchingBehaviorDefiningMatchingInvocationHandler(mockBehavior); return startMatchingInvocation(matchingInvocationHandler); } /** * Asserts that an invocation that matches the invocation following this call has been observed * on this mock object during this test. * * @return The proxy instance that will record the method call, not null */ @MatchStatement public T assertInvoked() { MatchingInvocationHandler matchingInvocationHandler = createAssertInvokedVerifyingMatchingInvocationHandler(); return startMatchingInvocation(matchingInvocationHandler); } /** * Asserts that an invocation that matches the invocation following this call has been observed * on this mock object during this test. * <p/> * If this method is used multiple times during the current test, the sequence of the observed method * calls has to be the same as the sequence of the calls to this method. * * @return The proxy instance that will record the method call, not null */ @MatchStatement public T assertInvokedInSequence() { MatchingInvocationHandler matchingInvocationHandler = createAssertInvokedInSequenceVerifyingMatchingInvocationHandler(); return startMatchingInvocation(matchingInvocationHandler); } /** * Asserts that no invocation that matches the invocation following this call has been observed * on this mock object during this test. * * @return The proxy instance that will record the method call, not null */ @MatchStatement public T assertNotInvoked() { MatchingInvocationHandler matchingInvocationHandler = createAssertNotInvokedVerifyingMatchingInvocationHandler(); return startMatchingInvocation(matchingInvocationHandler); } /** * Removes all behavior defined for this mock. * This will only remove the behavior, not the observed invocations for this mock. */ @MatchStatement public void resetBehavior() { oneTimeMatchingBehaviorDefiningInvocations.clear(); alwaysMatchingBehaviorDefiningInvocations.clear(); chainedMocksPerName.clear(); getMatchingInvocationBuilder().reset(); ArgumentMatcherRepository.getInstance().reset(); } @SuppressWarnings({"unchecked"}) public <M> Mock<M> createChainedMock(String name, Class<M> mockedType) { Mock<?> chainedMock = chainedMocksPerName.get(name); if (chainedMock != null) { return (Mock<M>) chainedMock; } try { if (Void.class.equals(mockedType) || mockedType.isPrimitive() || mockedType.isArray()) { return null; } chainedMock = new MockObject<M>(name, mockedType, getCurrentScenario().getTestObject()); chainedMocksPerName.put(name, chainedMock); return (Mock<M>) chainedMock; } catch (Throwable t) { return null; } } public String getName() { return name; } protected T startMatchingInvocation(MatchingInvocationHandler matchingInvocationHandler) { return getMatchingInvocationBuilder().startMatchingInvocation(name, mockedType, matchingInvocationHandler); } protected synchronized MatchingInvocationBuilder getMatchingInvocationBuilder() { MatchingInvocationBuilder matchingInvocationBuilder = matchingInvocationBuilderThreadLocal.get(); if (matchingInvocationBuilder == null) { matchingInvocationBuilder = createMatchingInvocationBuilder(); matchingInvocationBuilderThreadLocal.set(matchingInvocationBuilder); } return matchingInvocationBuilder; } protected synchronized Scenario getScenario(Object testObject) { Scenario scenario = scenarioThreadLocal.get(); if (scenario == null) { scenario = createScenario(testObject); scenarioThreadLocal.set(scenario); } return scenario; } protected MockProxy<T> createMockProxy() { return new MockProxy<T>(name, mockedType, oneTimeMatchingBehaviorDefiningInvocations, alwaysMatchingBehaviorDefiningInvocations, getCurrentScenario(), getMatchingInvocationBuilder()); } protected MatchingInvocationHandler createOneTimeMatchingBehaviorDefiningMatchingInvocationHandler(MockBehavior mockBehavior) { return new BehaviorDefiningMatchingInvocationHandler(mockBehavior, oneTimeMatchingBehaviorDefiningInvocations, this); } protected MatchingInvocationHandler createAlwaysMatchingBehaviorDefiningMatchingInvocationHandler(MockBehavior mockBehavior) { return new BehaviorDefiningMatchingInvocationHandler(mockBehavior, alwaysMatchingBehaviorDefiningInvocations, this); } protected BehaviorDefiningInvocations createOneTimeMatchingBehaviorDefiningInvocations() { return new BehaviorDefiningInvocations(true); } protected BehaviorDefiningInvocations createAlwaysMatchingBehaviorDefiningInvocations() { return new BehaviorDefiningInvocations(false); } protected MatchingInvocationHandler createAssertInvokedVerifyingMatchingInvocationHandler() { return new AssertInvokedVerifyingMatchingInvocationHandler(getCurrentScenario(), this); } protected MatchingInvocationHandler createAssertInvokedInSequenceVerifyingMatchingInvocationHandler() { return new AssertInvokedInSequenceVerifyingMatchingInvocationHandler(getCurrentScenario(), this); } protected MatchingInvocationHandler createAssertNotInvokedVerifyingMatchingInvocationHandler() { return new AssertNotInvokedVerifyingMatchingInvocationHandler(getCurrentScenario(), this); } protected Scenario createScenario(Object testObject) { return new Scenario(testObject); } protected MatchingInvocationBuilder createMatchingInvocationBuilder() { return new MatchingInvocationBuilder(); } }