Как издеваться над автоматически подключенным полем @Value весной с помощью Mockito?

125

Я использую Spring 3.1.4.RELEASE и Mockito 1.9.5. В моем классе Spring у меня есть:

@Value("#{myProps['default.url']}")
private String defaultUrl;

@Value("#{myProps['default.password']}")
private String defaultrPassword;

// ...

Из моего теста JUnit, который я сейчас настроил так:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 

Я хотел бы высмеять значение моего поля "defaultUrl". Обратите внимание, что я не хочу имитировать значения для других полей - я хотел бы оставить их такими, какие они есть, только поле «defaultUrl». Также обратите внимание, что у меня нет явных методов «установки» (например setDefaultUrl) в моем классе, и я не хочу создавать их только для целей тестирования.

Учитывая это, как я могу имитировать значение для этого поля?

Дейв
источник

Ответы:

145

Вы можете использовать магию Spring, ReflectionTestUtils.setFieldчтобы избежать каких-либо изменений в вашем коде.

Ознакомьтесь с этим руководством для получения дополнительной информации, хотя, вероятно, он вам не понадобится, поскольку метод очень прост в использовании.

ОБНОВИТЬ

С момента появления Spring 4.2.RC1 теперь можно установить статическое поле без необходимости предоставлять экземпляр класса. См. Эту часть документации и этот коммит.

geoand
источник
13
На всякий случай, если ссылка мертва: используйте ReflectionTestUtils.setField(bean, "fieldName", "value");перед вызовом beanметода во время теста.
Michał Stochmal
2
Хорошее решение для имитации свойств, извлекаемых из файла свойств.
Энтони Сампат Кумар Редди
@ MichałStochmal, это приведет к появлению, поскольку поле является закрытым java.lang.IllegalStateException: не удалось получить доступ к методу: класс org.springframework.util.ReflectionUtils не может получить доступ к члену класса com.kaleidofin.app.service.impl.CVLKRAProvider с модификаторами "" в org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) в org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Ахил Сурапурам
113

Это был уже третий раз, когда я гуглил этот пост SO, поскольку я всегда забываю, как издеваться над полем @Value. Хотя принятый ответ верен, мне всегда нужно время, чтобы правильно выполнить вызов "setField", поэтому, по крайней мере, для себя я вставляю здесь пример фрагмента:

Класс продукции:

@Value("#{myProps[‘some.default.url']}")
private String defaultUrl;

Тестовый класс:

import org.springframework.test.util.ReflectionTestUtils;

ReflectionTestUtils.setField(instanceUnderTest, "defaultUrl", "http://foo");
// Note: Don't use MyClassUnderTest.class, use the instance you are testing itself
// Note: Don't use the referenced string "#{myProps[‘some.default.url']}", 
//       but simply the FIELDs name ("defaultUrl")
BAERUS
источник
32

Вы также можете смоделировать конфигурацию своего свойства в своем тестовом классе

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 
   @Configuration
   public static class MockConfig{
       @Bean
       public Properties myProps(){
             Properties properties = new Properties();
             properties.setProperty("default.url", "myUrl");
             properties.setProperty("property.value2", "value2");
             return properties;
        }
   }
   @Value("#{myProps['default.url']}")
   private String defaultUrl;

   @Test
   public void testValue(){
       Assert.assertEquals("myUrl", defaultUrl);
   }
}
Мануэль Хинонес
источник
26

Я хотел бы предложить связанное решение, которое заключается в передаче @Valueаннотированных полей в качестве параметров конструктору вместо использования ReflectionTestUtilsкласса.

Вместо этого:

public class Foo {

    @Value("${foo}")
    private String foo;
}

и

public class FooTest {

    @InjectMocks
    private Foo foo;

    @Before
    public void setUp() {
        ReflectionTestUtils.setField(Foo.class, "foo", "foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

Сделай это:

public class Foo {

    private String foo;

    public Foo(@Value("${foo}") String foo) {
        this.foo = foo;
    }
}

и

public class FooTest {

    private Foo foo;

    @Before
    public void setUp() {
        foo = new Foo("foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

Преимущества этого подхода: 1) мы можем создать экземпляр класса Foo без контейнера зависимостей (это просто конструктор) и 2) мы не связываем наш тест с деталями нашей реализации (отражение привязывает нас к имени поля с помощью строки, что может вызвать проблемы, если мы изменим имя поля).

отметка
источник
3
обратная сторона: если кто-то испортит аннотацию, например, использует свойство bar вместо foo, ваш тест все равно будет работать. Просто у меня есть этот чехол.
Нильс Эль-Химуд
@ NilsEl-Himoud В целом это справедливый вопрос для вопроса OP, но проблема, которую вы поднимаете, не лучше и не хуже, используя утилиты отражения и конструктор. Смысл этого ответа заключался в рассмотрении конструктора над отражением util (принятый ответ). Марк, спасибо за ответ, ценю легкость и чистоту этого твика.
Marquee
22

Вы можете использовать эту волшебную аннотацию Spring Test:

@TestPropertySource(properties = { "my.spring.property=20" }) 

см. org.springframework.test.context.TestPropertySource

Например, это тестовый класс:

@ContextConfiguration(classes = { MyTestClass.Config.class })
@TestPropertySource(properties = { "my.spring.property=20" })
public class MyTestClass {

  public static class Config {
    @Bean
    MyClass getMyClass() {
      return new MyClass ();
    }
  }

  @Resource
  private MyClass myClass ;

  @Test
  public void myTest() {
   ...

А это класс со свойством:

@Component
public class MyClass {

  @Value("${my.spring.property}")
  private int mySpringProperty;
   ...
Тибо
источник
14

Я использовал приведенный ниже код, и он сработал для меня:

@InjectMocks
private ClassABC classABC;

@Before
public void setUp() {
    ReflectionTestUtils.setField(classABC, "constantFromConfigFile", 3);
}

Ссылка: https://www.jeejava.com/mock-an-autowired-value-field-in-spring-with-junit-mockito/

Мендон Ашвини
источник
1
то же самое здесь ... +1
Нежный
я вроде сделал то же самое, но это все еще не отражается
Шубхро Мукерджи
1

Также обратите внимание, что у меня нет явных методов «установки» (например, setDefaultUrl) в моем классе, и я не хочу создавать их только для целей тестирования.

Один из способов решить эту проблему - изменить свой класс на использование Constructor Injection , которое используется для тестирования и Spring инъекции. Больше никаких размышлений :)

Итак, вы можете передать любую строку с помощью конструктора:

class MySpringClass {

    private final String defaultUrl;
    private final String defaultrPassword;

    public MySpringClass (
         @Value("#{myProps['default.url']}") String defaultUrl, 
         @Value("#{myProps['default.password']}") String defaultrPassword) {
        this.defaultUrl = defaultUrl;
        this.defaultrPassword= defaultrPassword;
    }

}

И в своем тесте просто используйте это:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
Dherik
источник