Jak wiosną mockować pole autowired @Value za pomocą Mockito?

124

Używam Springa 3.1.4.RELEASE i Mockito 1.9.5. Na moich zajęciach wiosennych mam:

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

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

// ...

Z mojego testu JUnit, który obecnie skonfigurowałem w następujący sposób:

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

Chciałbym sfałszować wartość mojego pola „defaultUrl”. Zauważ, że nie chcę naśladować wartości dla innych pól - chciałbym zachować je takimi, jakimi są, tylko pole „defaultUrl”. Zauważ również, że nie mam jawnych metod "ustawiających" (np. setDefaultUrl) W mojej klasie i nie chcę tworzyć żadnych tylko do celów testowych.

Biorąc to pod uwagę, jak mogę zakpić wartość dla tego jednego pola?

Dave
źródło

Odpowiedzi:

145

Możesz użyć magii Springa ReflectionTestUtils.setField, aby uniknąć jakichkolwiek modyfikacji kodu.

Sprawdź ten samouczek, aby uzyskać jeszcze więcej informacji, chociaż prawdopodobnie nie będziesz ich potrzebować, ponieważ metoda jest bardzo łatwa w użyciu

AKTUALIZACJA

Od czasu wprowadzenia Springa 4.2.RC1 możliwe jest teraz ustawienie pola statycznego bez konieczności dostarczania instancji klasy. Zobacz część dokumentacji i to zatwierdzenie.

geoand
źródło
12
Na wypadek, gdyby link był martwy: użyj go ReflectionTestUtils.setField(bean, "fieldName", "value");przed wywołaniem beanmetody podczas testu.
Michał Stochmal
2
Dobre rozwiązanie do naśladowania właściwości, które są pobierane z pliku właściwości.
Antony Sampath Kumar Reddy
@ MichałStochmal, zrobienie tego spowoduje, że plik jest prywatny java.lang.IllegalStateException: Nie można uzyskać dostępu do metody: Klasa org.springframework.util.ReflectionUtils nie może uzyskać dostępu do elementu członkowskiego klasy com.kaleidofin.app.service.impl.CVLKRAProvider z modyfikatorami „” na org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) na org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Akhil Surapuram
113

To był trzeci raz, kiedy googlowałem do tego wpisu SO, ponieważ zawsze zapominam, jak kpić z pola @Value. Chociaż zaakceptowana odpowiedź jest poprawna, zawsze potrzebuję trochę czasu, aby uzyskać prawidłowe wywołanie „setField”, więc przynajmniej dla siebie wklejam tutaj przykładowy fragment:

Klasa produkcyjna:

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

Klasa testu:

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
źródło
32

Możesz również mockować konfigurację właściwości w klasie testowej

@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);
   }
}
Manuel Quinones
źródło
25

Chciałbym zasugerować pokrewne rozwiązanie, @Valuepolegające na przekazaniu pól z adnotacjami jako parametrów do konstruktora zamiast używania ReflectionTestUtilsklasy.

Zamiast tego:

public class Foo {

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

i

public class FooTest {

    @InjectMocks
    private Foo foo;

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

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

Zrób to:

public class Foo {

    private String foo;

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

i

public class FooTest {

    private Foo foo;

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

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

Korzyści z tego podejścia: 1) możemy utworzyć instancję klasy Foo bez kontenera zależności (to tylko konstruktor), oraz 2) nie łączymy naszego testu z naszymi szczegółami implementacji (odbicie wiąże nas z nazwą pola za pomocą łańcucha, co może spowodować problem, jeśli zmienimy nazwę pola).

znak
źródło
3
minus: jeśli ktoś pomyli się z adnotacją, np. użyje właściwości „bar” zamiast „foo”, test będzie nadal działał. Mam tylko tę walizkę.
Nils El-Himoud
@ NilsEl-Himoud To ogólnie rzecz biorąc słuszna uwaga, jeśli chodzi o pytanie OP, ale problem, który poruszysz, nie jest lepszy ani gorszy przy użyciu narzędzi refleksyjnych w porównaniu z konstruktorem. Celem tej odpowiedzi było rozważenie konstruktora nad refleksją (przyjęta odpowiedź). Mark, dzięki za odpowiedź, doceniam łatwość i czystość tej poprawki.
Marquee
22

Możesz użyć tej magicznej adnotacji Spring Test:

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

zobacz org.springframework.test.context.TestPropertySource

Na przykład jest to klasa testowa:

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

A to jest klasa z właściwością:

@Component
public class MyClass {

  @Value("${my.spring.property}")
  private int mySpringProperty;
   ...
Thibault
źródło
1

Zauważ też, że nie mam żadnych jawnych metod "ustawiających" (np. SetDefaultUrl) w mojej klasie i nie chcę tworzyć żadnych tylko dla celów testowych.

Jednym ze sposobów rozwiązania tego problemu jest zmiana klasy tak, aby używała funkcji Constructor Injection , która jest używana do testowania i wtrysku sprężyny. Nigdy więcej refleksji :)

Możesz więc przekazać dowolny ciąg przy użyciu konstruktora:

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

}

W swoim teście po prostu go użyj:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
Dherik
źródło