¿Cómo me burlo de un campo @Value conectado automáticamente en Spring con Mockito?

124

Estoy usando Spring 3.1.4.RELEASE y Mockito 1.9.5. En mi clase de primavera tengo:

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

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

// ...

De mi prueba JUnit, que actualmente he configurado así:

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

Me gustaría simular un valor para mi campo "defaultUrl". Tenga en cuenta que no quiero simular valores para los otros campos; me gustaría mantenerlos como están, solo el campo "defaultUrl". También tenga en cuenta que no tengo ningún método "establecedor" explícito (por ejemplo setDefaultUrl) en mi clase y no quiero crear ninguno solo para fines de prueba.

Dado esto, ¿cómo puedo simular un valor para ese campo?

Dave
fuente

Respuestas:

145

Puede utilizar la magia de Spring ReflectionTestUtils.setFieldpara evitar realizar modificaciones de cualquier tipo en su código.

Consulte este tutorial para obtener aún más información, aunque probablemente no lo necesite ya que el método es muy fácil de usar.

ACTUALIZAR

Desde la introducción de Spring 4.2.RC1, ahora es posible establecer un campo estático sin tener que proporcionar una instancia de la clase. Vea esta parte de la documentación y este compromiso.

geo y
fuente
12
Solo en caso de que el enlace esté inactivo: utilícelo ReflectionTestUtils.setField(bean, "fieldName", "value");antes de invocar su beanmétodo durante la prueba.
Michał Stochmal
2
Buena solución para burlarse de las propiedades que se recuperan del archivo de propiedades.
Antony Sampath Kumar Reddy
@ MichałStochmal, hacer eso producirá ya que el archivo es privado java.lang.IllegalStateException: No se pudo acceder al método: La clase org.springframework.util.ReflectionUtils no puede acceder a un miembro de la clase com.kaleidofin.app.service.impl.CVLKRAProvider con modificadores "" en org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) en org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Akhil Surapuram
113

Ahora era la tercera vez que buscaba en Google esta publicación de SO, ya que siempre olvido cómo burlarme de un campo @Value. Aunque la respuesta aceptada es correcta, siempre necesito algo de tiempo para que la llamada "setField" sea correcta, así que al menos para mí pego un fragmento de ejemplo aquí:

Clase de producción:

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

Clase de prueba:

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
fuente
32

También puede simular la configuración de su propiedad en su clase de prueba

@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 Quiñones
fuente
25

Me gustaría sugerir una solución relacionada, que es pasar los @Valuecampos anotados como parámetros al constructor, en lugar de usar la ReflectionTestUtilsclase.

En lugar de esto:

public class Foo {

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

y

public class FooTest {

    @InjectMocks
    private Foo foo;

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

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

Hacer esto:

public class Foo {

    private String foo;

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

y

public class FooTest {

    private Foo foo;

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

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

Beneficios de este enfoque: 1) podemos crear una instancia de la clase Foo sin un contenedor de dependencia (es solo un constructor), y 2) no estamos acoplando nuestra prueba a los detalles de nuestra implementación (la reflexión nos vincula al nombre del campo usando una cadena, que podría causar un problema si cambiamos el nombre del campo).

marca
fuente
3
inconveniente: si alguien se mete con la anotación, por ejemplo, utiliza una propiedad 'bar' en lugar de 'foo', la prueba seguirá funcionando. Solo tengo este caso.
Nils El-Himoud
@ NilsEl-Himoud Ese es un punto justo en general para la pregunta de OP, pero el problema que plantea no es mejor ni peor si se usa la utilidad de reflexión frente al constructor. El punto de esta respuesta fue la consideración del constructor sobre la reflexión util (la respuesta aceptada). Mark, gracias por la respuesta, agradezco la facilidad y limpieza de este ajuste.
Marquesina
22

Puede usar esta anotación mágica de Spring Test:

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

ver org.springframework.test.context.TestPropertySource

Por ejemplo, esta es la clase de prueba:

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

Y esta es la clase con la propiedad:

@Component
public class MyClass {

  @Value("${my.spring.property}")
  private int mySpringProperty;
   ...
Thibault
fuente
1

También tenga en cuenta que no tengo métodos explícitos "setter" (por ejemplo, setDefaultUrl) en mi clase y no quiero crear ninguno solo con el propósito de probar.

Una forma de resolver esto es cambiar su clase para usar Constructor Injection , que se usa para pruebas e inyección Spring. No más reflexión :)

Entonces, puede pasar cualquier String usando el constructor:

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

}

Y en tu prueba, solo úsala:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
Dherik
fuente