Внедрение значения 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 при запуске теста. Мне было интересно, есть ли способ сделать это.

Спасибо


person rohit    schedule 09.02.2012    source источник
comment
Если вы издеваетесь над CustomService, то как используется CustomServiceImpl? Это не имеет смысла.   -  person skaffman    schedule 09.02.2012
comment
Я использую CustomeServiceImpl. Обновил код. Как я могу издеваться над значением URL, которое нужно прочитать из файла свойств?   -  person rohit    schedule 09.02.2012
comment
В настоящее время вы не используете Spring для установки значения customService, вы устанавливаете значение вручную в методе setup() с помощью этого кода: customService = new CustomServiceImpl();   -  person DwB    schedule 09.02.2012
comment
Нет, я не использую Spring для установки значения customService для тестов, но в реальном приложении я использую Spring для установки значения. Можно ли издеваться над значением URL, как я делаю это для dataService?   -  person rohit    schedule 09.02.2012


Ответы (6)


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

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

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

person Joseph Lust    schedule 19.09.2013
comment
вы знаете, работает ли это также с аннотацией @Resource? - person OscarRyz; 12.12.2014

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

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

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

EDIT: Теперь в вашем коде вы напрямую устанавливаете CustomerServiceImpl в своем блоке перед, что также не имеет смысла. Весна тут ни при чем!

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

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

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

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

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

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

person Brice    schedule 09.02.2012
comment
Под пересмотром использования Spring вы подразумеваете пересмотр использования файлов конфигурации Spring для внедрения зависимостей для этого теста, верно? Вы не предлагаете ему отказаться от использования Spring в своем приложении. - person jhericks; 10.02.2012
comment
@jhericks Нет, я хотел удалить код, связанный с Spring, в модульном тесте. Обычно я придерживаюсь жесткой позиции по этому поводу: я не одобряю использование связующего кода, такого как Sprin, в любых модульных тестах. Целью модульного тестирования является изолированное тестирование производственного кода (здесь CustomerServiceImpl). Собственно, это и есть замысел автора темы. Если по какой-то причине требуется spring, то он становится интеграционным тестом, который не имеет таких последствий в тесте. - person Brice; 10.02.2012

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

person John B    schedule 09.02.2012
comment
Не совершайте смертный грех модульного тестирования. - person Joseph Lust; 20.09.2013

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

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

Вы можете использовать этот небольшой служебный класс (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 для разрешения всех значений заполнителей. Вы также можете изменить тип параметра 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");, поскольку вам не нужно полагаться на жестко заданные имена полей.

person Jan Gassen    schedule 04.12.2018

person    schedule
comment
Не могли бы вы также объяснить свое решение в нескольких словах? - person Magnilex; 21.04.2015
comment
Вопрос от Rohit может иметь другие проблемы, но при поиске решения моей проблемы это решение было именно тем, что я хотел, спасибо, Роберт. - person mikeapr4; 08.07.2015
comment
то же самое с groovy? - person Rocky4Ever; 30.01.2019
comment
да, в groovy должно быть то же самое, за исключением некоторых незначительных различий в синтаксисе между java и groovy. все весенние библиотеки должны быть доступны для использования в groovy-проекте. - person Robert Hutto; 02.02.2019
comment
@InjectMock без s в конце? Из какой упаковки эта штука? - person kiltek; 04.04.2020
comment
Используйте аннотацию org.junit.jupiter.api.BeforeEach вместо @Before для JUnit5 - person maheeka; 19.05.2020
comment
@InjectMocks мокито. Спасибо за улов - person Robert Hutto; 25.09.2020