Adding a new JUnit rule to help with cleaning up spring singleton beans whose backend services have been mocked out.

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@39395 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Neil McErlean
2012-07-17 20:48:05 +00:00
parent cff0e3696f
commit ac7c5d5a4f
2 changed files with 272 additions and 0 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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.
*
* <p/>
* Example usage:
* <pre>
* public class YourTestClass
* {
* // Declare the rule.
* &#64;Rule public final TemporaryMockOverride mockOverride = new TemporaryMockOverride();
*
* &#64;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.
* }
* }
* </pre>
*
* @author Neil Mc Erlean
* @since Odin
*/
public class TemporaryMockOverride extends ExternalResource
{
private static final Log log = LogFactory.getLog(TemporaryMockOverride.class);
private List<FieldValueOverride> pristineFieldValues = new ArrayList<FieldValueOverride>();
@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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; } }
}