diff --git a/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverride.java b/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverride.java new file mode 100644 index 0000000000..e81243119f --- /dev/null +++ b/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverride.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2005-2012 + Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.test.junitrules; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.rules.ExternalResource; +import org.springframework.util.ReflectionUtils; + +/** + * A JUnit rule designed to help with the automatic revert of test objects with mocked fields. + * This is intended to be used when writing test code and you wish to set a mock object on a spring singleton bean. + * By mocking out fields on spring singletons beans, if you don't remember to revert them to their original values, then + * any subsequent tests that expect to see 'proper' services plugged in, may fail when they are instead given a mock. + * + *

+ * Example usage: + *

+ * public class YourTestClass
+ * {
+ *     // Declare the rule.
+ *     @Rule public final TemporaryMockOverride mockOverride = new TemporaryMockOverride();
+ *     
+ *     @Test public void aTestMethod()
+ *     {
+ *         // Get a singleton bean from the spring context
+ *         FooService fooService = appContext.getBean("fooService", FooService.class);
+ *         
+ *         // Create a mocked service (this uses Mockito, but that's not required for this rule to work)
+ *         BarService mockedBarService = mock(BarService.class);
+ *         
+ *         // Don't do this as you're just replacing the original barService: fooService.setBarService(mockedBarService);
+ *         // Instead do this:
+ *         mockOverride.setTemporaryField(fooService, barService, mockedBarService);
+ *         
+ *         // Go ahead and use the FooService in test code, whilst relying on a mocked BarService behind it.
+ *         // After the rule has completed, the original BarService which spring injected into the FooService will be reset.
+ *     }
+ * }
+ * 
+ * + * @author Neil Mc Erlean + * @since Odin + */ +public class TemporaryMockOverride extends ExternalResource +{ + private static final Log log = LogFactory.getLog(TemporaryMockOverride.class); + + private List pristineFieldValues = new ArrayList(); + + @Override protected void before() throws Throwable + { + // Intentionally empty + } + + @Override protected void after() + { + // For all objects that have been tampered with, we'll revert them to their original state. + for (int i = pristineFieldValues.size() - 1; i >= 0; i-- ) + { + FieldValueOverride override = pristineFieldValues.get(i); + + if (log.isDebugEnabled()) + { + log.debug("Reverting mocked field '" + override.fieldName + "' on object " + override.objectContainingField + " to original value '" + override.fieldPristineValue + "'"); + } + + // Hack into the Java field object + Field f = ReflectionUtils.findField(override.objectContainingField.getClass(), override.fieldName); + ReflectionUtils.makeAccessible(f); + // and revert its value. + ReflectionUtils.setField(f, override.objectContainingField, override.fieldPristineValue); + } + } + + public void setTemporaryField(Object objectContainingField, String fieldName, Object fieldValue) + { + if (log.isDebugEnabled()) + { + log.debug("Overriding field '" + fieldName + "' on object " + objectContainingField + " to new value '" + fieldValue + "'"); + } + + // Extract the pristine value of the field we're going to mock. + Field f = ReflectionUtils.findField(objectContainingField.getClass(), fieldName); + + if (f == null) + { + final String msg = "Object of type '" + objectContainingField.getClass().getSimpleName() + "' has no field named '" + fieldName + "'"; + if (log.isDebugEnabled()) + { + log.debug(msg); + } + throw new IllegalArgumentException(msg); + } + + ReflectionUtils.makeAccessible(f); + Object pristineValue = ReflectionUtils.getField(f, objectContainingField); + + // and add it to the list. + pristineFieldValues.add(new FieldValueOverride(objectContainingField, fieldName, pristineValue)); + + // and set it on the object + ReflectionUtils.setField(f, objectContainingField, fieldValue); + } + + private static class FieldValueOverride + { + public FieldValueOverride(Object objectContainingField, String fieldName, Object pristineValue) + { + this.objectContainingField = objectContainingField; + this.fieldName = fieldName; + this.fieldPristineValue = pristineValue; + } + + public final Object objectContainingField; + public final String fieldName; + public final Object fieldPristineValue; + } +} diff --git a/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverrideTest.java b/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverrideTest.java new file mode 100644 index 0000000000..201bcbb67c --- /dev/null +++ b/source/java/org/alfresco/util/test/junitrules/TemporaryMockOverrideTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.test.junitrules; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +/** + * Test class for {@link TemporaryMockOverride}. + * + * @author Neil McErlean + * @since Odin + */ +public class TemporaryMockOverrideTest +{ + private static final String REAL_DATA = "Hello"; + private static final String MOCKED_DATA = "--"; + + private final FooService realFooService = new FooServiceImpl(); + private final BarService realBarService = new BarServiceImpl(); + private final AbcService realAbcService = new AbcServiceImpl(); + + @Before public void init() + { + FooServiceImpl fooServiceImpl = (FooServiceImpl)realFooService; + fooServiceImpl.setBarService(realBarService); + fooServiceImpl.setAbcService(realAbcService); + } + + @Test public void mockFieldsWithinServiceAndThenEnsureTheProperFieldValuesAreRestoredAfterCleanup() throws Throwable + { + TemporaryMockOverride mockRule = new TemporaryMockOverride(); + mockRule.before(); + + assertEquals("Original BarService giving wrong data.", REAL_DATA, realFooService.getBarService().getString()); + assertEquals("Original AbcService giving wrong data.", REAL_DATA, realFooService.getAbcService().getString()); + + BarService mockedBarService = mock(BarService.class); + when(mockedBarService.getString()).thenReturn(MOCKED_DATA); + + AbcService mockedAbcService = mock(AbcService.class); + when(mockedAbcService.getString()).thenReturn(MOCKED_DATA); + + FooServiceImpl fooServiceWithMockedServices = new FooServiceImpl(); + // We'll start it off with the 'correct' values + fooServiceWithMockedServices.setBarService(realBarService); + fooServiceWithMockedServices.setAbcService(realAbcService); + + // ...and then set the mocked values via the rule, which will remember the old values and revert them for us automatically. + mockRule.setTemporaryField(fooServiceWithMockedServices, "barService", mockedBarService); + mockRule.setTemporaryField(fooServiceWithMockedServices, "abcService", mockedAbcService); + + + assertEquals("Mocked BarService giving wrong data.", MOCKED_DATA, fooServiceWithMockedServices.getBarService().getString()); + assertEquals("Mocked AbcService giving wrong data.", MOCKED_DATA, fooServiceWithMockedServices.getAbcService().getString()); + + mockRule.after(); + + // Now it should all be magically reverted. + assertEquals("BarService giving wrong data.", REAL_DATA, fooServiceWithMockedServices.getBarService().getString()); + assertEquals("AbcService giving wrong data.", REAL_DATA, fooServiceWithMockedServices.getAbcService().getString()); + } + + @Test(expected=IllegalArgumentException.class) public void mockNonExistentFieldsWithinService() throws Throwable + { + TemporaryMockOverride mockRule = new TemporaryMockOverride(); + mockRule.before(); + + assertEquals("Original BarService giving wrong data.", REAL_DATA, realFooService.getBarService().getString()); + + BarService mockedBarService = mock(BarService.class); + when(mockedBarService.getString()).thenReturn(MOCKED_DATA); + + FooServiceImpl fooServiceWithMockedServices = new FooServiceImpl(); + // We'll start it off with the 'correct' values + fooServiceWithMockedServices.setBarService(realBarService); + + // ...and then set an illegal mocked value via the rule. + mockRule.setTemporaryField(fooServiceWithMockedServices, "noSuchService", mockedBarService); + + + assertEquals("Mocked BarService giving wrong data.", MOCKED_DATA, fooServiceWithMockedServices.getBarService().getString()); + + mockRule.after(); + + // Now it should all be magically reverted. + assertEquals("BarService giving wrong data.", REAL_DATA, fooServiceWithMockedServices.getBarService().getString()); + } + + public interface FooService { public BarService getBarService(); public AbcService getAbcService(); } + + public class FooServiceImpl implements FooService + { + private BarService barService; + private AbcService abcService; + + public BarService getBarService() { return this.barService; } + public void setBarService(BarService barService) { this.barService = barService; } + public AbcService getAbcService() { return this.abcService; } + public void setAbcService(AbcService abcService) { this.abcService = abcService; } + } + + public interface BarService { public String getString(); } + + public class BarServiceImpl implements BarService { public String getString() { return REAL_DATA; } } + + public interface AbcService { public String getString(); } + + public class AbcServiceImpl implements AbcService { public String getString() { return REAL_DATA; } } +} +