Spring инъекция значения в mockito

Я пытаюсь написать тестовый класс для следующего метода

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

Я использую введенное значение url в одном из методов в классе. Чтобы проверить это, я написал класс junit

@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);
    }
}

В applicationContext-test.xml я загружаю файл свойств, используя

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

Но значение url не загружается в CustomService при запуске теста. Мне было интересно, если все-таки это сделать.

Спасибо

Ответы

Ответ 1

Вы можете autowire в мутаторе (сеттер), а не просто аннотировать частное поле. Затем вы можете использовать этот сеттер из своего тестового класса. Нет необходимости делать это общедоступным, пакет private будет делать, поскольку Spring может все же получить к нему доступ, но в противном случае может быть получен только ваш тест (или другой код в том же пакете).

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

Я не поклонник autowiring по-разному (по сравнению с моей кодовой базой) только для тестирования, но альтернатива изменения тестируемого класса из теста просто нечестива.

Ответ 2

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");
}    
...
}

Ответ 3

Я согласен с комментарием @skaffman.

Кроме того, в вашем тесте используется MockitoJUnitRunner, поэтому он не будет искать какой-либо материал Spring, это единственная цель - инициализировать Mockito mocks. ContextConfiguration недостаточно для проводки событий с помощью spring. Технически с JUnit вы можете использовать следующий бегун, если хотите Spring связанные вещи: SpringJUnit4ClassRunner.

Также, когда вы пишете Unit Test, вам может потребоваться пересмотреть использование spring. Использование проводки Spring в unit test неверно. Однако, если вы вместо этого пишете Тест интеграции, то почему вы используете Mockito там, это не имеет смысла (как сказал скаффман)!

РЕДАКТИРОВАТЬ: Теперь в вашем коде вы прямо устанавливаете CustomerServiceImpl в своем блоке до этого, это тоже не имеет смысла. Spring вообще не участвует!

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

РЕДАКТИРОВАТЬ 2: Если вы хотите написать Unit Test of CustomerServiceImpl, то не используйте материал Spring и сразу вводите значение свойства. Также вы можете использовать Mockito для вставки DataService mock straigth в тестируемый экземпляр.

@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() { ... }
}

Как вы могли заметить, я использую прямой доступ к полю url, поле может быть видимым пакетом. Это тестовое обходное решение, чтобы на самом деле ввести значение URL, поскольку Mockito вводит только mocks.

Ответ 4

Вы не должны издеваться над тем, что вы пытаетесь проверить. Это бессмысленно, поскольку вы не будете касаться какого-либо кода, который вы пытаетесь проверить. Вместо этого получите экземпляр CustomerServiceImpl из контекста.

Ответ 5

У меня был список строк из файла свойств. Метод ReflectionTestUtils class SetField, используемый в блоке @Before, помог мне установить эти значения до моего выполнения теста. Он отлично работал даже для моего слоя dao, который зависит от класса Common DaoSupport.

@Before
public void setList() {
    List<String> mockedList = new ArrayList<>();
    mockedSimList.add("CMS");
    mockedSimList.add("SDP");
    ReflectionTestUtils.setField(mockedController, "ActualListInController",
            mockedList);
}

Ответ 6

Вы можете использовать этот небольшой класс утилиты (gist) для автоматического ввода значений полей в целевой класс:

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);
  }
}

Он использует org.apache.commons.lang3.reflect.FieldUtils для доступа ко всем полям, аннотированным с помощью @Value а затем использует Spring-классы для решения всех значений placeholder. Вы также можете изменить тип properties параметра на PlaceholderResolver если хотите использовать свой собственный PlaceholderResolver. В своем тесте вы можете использовать его для ввода набора значений, заданных как экземпляр Map или Properties например, в следующем примере:

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);

Это будет пытаться решить все @Value аннотированных полей в вашем dataService. Я лично предпочитаю это решение через ReflectionTestUtils.setField(dataService, "field", "value"); так как вам не нужно полагаться на имена жестко закодированных полей.