Spring value injection in mockito

2020-02-23 06:19发布

I'm trying to write test class for the following method

public class CustomServiceImpl implements CustomService {
    @Value("#{myProp['custom.url']}")
    private String url;
    @Autowire
    private DataService dataService;

I'm using the injected url value in one of the methods in the class. To test this i've written a junit class

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public CustomServiceTest{
    private CustomService customService;
    @Mock
    private DataService dataService;
    @Before
    public void setup() {
        customService = new CustomServiceImpl();
        Setter.set(customService, "dataService", dataService);
    }    
    ...
}

public class Setter {
    public static void set(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

In applicationContext-test.xml I'm loading the property file using

    <util:properties id="myProp" location="myProp.properties"/>

But the url value is not getting loaded in the CustomService on running the test. I was wondering if there is anyway to get this done.

Thanks

6条回答
我想做一个坏孩纸
2楼-- · 2020-02-23 06:52

You can autowire into a mutator (setter), rather than just annotating the private field. Then you can use that setter from your test class as well. No need to make it public, package private will do as Spring can still access it, but otherwise only your test can get in there (or other code in the same package).

@Value("#{myProp['custom.url']}")
String setUrl( final String url ) {
    this.url  = url;
}

I'm not a fan of autowiring differently (compared to my codebase) just for testing, but the alternative of changing the class under test, from the test, is simply unholy.

查看更多
手持菜刀,她持情操
3楼-- · 2020-02-23 06:55
import org.springframework.test.util.ReflectionTestUtils;

@RunWith(MockitoJUnitRunner.class)
public CustomServiceTest{

@InjectMock
private CustomServiceImpl customService;

@Mock
private DataService dataService;

@Before
public void setup() {
    ReflectionTestUtils.setField(customService, "url", "http://someurl");
}    
...
}
查看更多
劫难
4楼-- · 2020-02-23 07:03

I agree with the comment of @skaffman.

Besides your test uses the MockitoJUnitRunner, hence it won't look for any Spring stuff, this only purpose is to initialize Mockito mocks. The ContextConfiguration is not enough to wire things with spring. Technically with JUnit you can use the following runner if you want spring related stuff : SpringJUnit4ClassRunner.

Also as you are writing a Unit Test you might want to reconsider the use of spring. Using spring wiring in a unit test is wrong. However if you are instead writing an Integration Test then why are you using Mockito there, it doesn't make sense (as said by skaffman) !

EDIT: Now in your code your a directly setting the CustomerServiceImpl in your before block, that doesn't makes sense either. Spring is not involved at all there !

@Before
public void setup() {
    customService = new CustomServiceImpl();
    Setter.set(customService, "dataService", dataService);
}

EDIT 2: If you want to write a Unit Test of CustomerServiceImpl, then avoid Spring stuff and inject directly the value of the property. Also you could use Mockito to inject the DataService mock straigth into the tested instance.

@RunWith(MockitoJUnitRunner.class)
public CustomServiceImplTest{
    @InjectMocks private CustomServiceImpl customService;
    @Mock private DataService dataService;

    @Before void inject_url() { customerServiceImpl.url = "http://..."; }

    @Test public void customerService_should_delegate_to_dataService() { ... }
}

As you might have noticed I'm using a direct access to the url field, the field can be package visible. This is a test workaround to actually inject the URL value as Mockito only injects mocks.

查看更多
SAY GOODBYE
5楼-- · 2020-02-23 07:07

I had a List of strings reading from Properties file. ReflectionTestUtils class setField method used in @Before block helped me set these values prior to my test execution . It worked perfect even for my dao layer which is depending on Common DaoSupport class.

@Before
public void setList() {
    List<String> mockedList = new ArrayList<>();
    mockedSimList.add("CMS");
    mockedSimList.add("SDP");
    ReflectionTestUtils.setField(mockedController, "ActualListInController",
            mockedList);
}
查看更多
三岁会撩人
6楼-- · 2020-02-23 07:09

You should not mock the thing that you are trying to test. That is pointless since you would not be touching any of the code you are trying to test. Instead get the instance of CustomerServiceImpl from the context.

查看更多
Bombasti
7楼-- · 2020-02-23 07:12

You can use this little utility class (gist) to automatically inject field values into a target class:

public class ValueInjectionUtils {
  private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
  private static final ConversionService CONVERSION_SERVICE = new DefaultConversionService();
  private static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
      new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, SystemPropertyUtils.PLACEHOLDER_SUFFIX,
          SystemPropertyUtils.VALUE_SEPARATOR, true);

  public static void injectFieldValues(Object testClassInstance, Properties properties) {
    for (Field field : FieldUtils.getFieldsListWithAnnotation(testClassInstance.getClass(), Value.class)) {
      String value = field.getAnnotation(Value.class).value();
      if (value != null) {
        try {
          Object resolvedValue = resolveValue(value, properties);
          FieldUtils.writeField(field, testClassInstance, CONVERSION_SERVICE.convert(resolvedValue, field.getType()),
              true);
        } catch (IllegalAccessException e) {
          throw new IllegalStateException(e);
        }
      }
    }
  }

  private static Object resolveValue(String value, Properties properties) {
    String replacedPlaceholderString = PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(value, properties);
    return evaluateSpEL(replacedPlaceholderString, properties);
  }

  private static Object evaluateSpEL(String value, Properties properties) {
    Expression expression = EXPRESSION_PARSER.parseExpression(value, new TemplateParserContext());
    EvaluationContext context =
        SimpleEvaluationContext.forPropertyAccessors(new MapAccessor()).withRootObject(properties).build();
    return expression.getValue(context);
  }
}

It uses org.apache.commons.lang3.reflect.FieldUtils to access all fields annotated with @Value and then uses Spring utility classes to resolve all placeholder values. You can also change the type of parameter properties to PlaceholderResolver in case you like to use your own PlaceholderResolver. In your test, you can use it to inject a set of values given as a Map or Properties instance like in the following example:

HashMap<String, Object> props = new HashMap<>();
props.put("custom.url", "http://some.url");

Properties properties = new Properties();
properties.put("myProp", props);

ValueInjectionUtils.injectFieldValues(testTarget, properties);

This will then try to resolve all @Value annotated fields in your dataService. I personally prefer this solution over ReflectionTestUtils.setField(dataService, "field", "value"); as you don't have to rely on hardcoded field names.

查看更多
登录 后发表回答