Poblando Spring @Value durante la prueba de la unidad

238

Estoy tratando de escribir una Prueba de Unidad para un bean simple que se usa en mi programa para validar formularios. El bean se anota con @Componenty tiene una variable de clase que se inicializa usando

@Value("${this.property.value}") private String thisProperty;

Me gustaría escribir pruebas unitarias para los métodos de validación dentro de esta clase, sin embargo, si es posible, me gustaría hacerlo sin utilizar el archivo de propiedades. Mi razonamiento detrás de esto es que si el valor que obtengo del archivo de propiedades cambia, me gustaría que eso no afecte mi caso de prueba. Mi caso de prueba es probar el código que valida el valor, no el valor en sí.

¿Hay alguna manera de usar el código Java dentro de mi clase de prueba para inicializar una clase Java y llenar la propiedad Spring @Value dentro de esa clase y luego usar eso para probar?

Encontré este How To que parece estar cerca, pero todavía usa un archivo de propiedades. Prefiero que todo sea código Java.

Kyle
fuente
He descrito una solución aquí para un problema similar. Espero eso ayude.
horizon7

Respuestas:

199

Si es posible, trataría de escribir esas pruebas sin Spring Context. Si crea esta clase en su prueba sin resorte, entonces tiene control total sobre sus campos.

Para configurar el @valuecampo, puede usar Springs ReflectionTestUtils: tiene un método setFieldpara configurar campos privados.

@ver JavaDoc : ReflectionTestUtils.setField (java.lang.Object, java.lang.String, java.lang.Object)

Ralph
fuente
2
Exactamente lo que estaba tratando de hacer y lo que estaba buscando para establecer el valor dentro de mi clase, ¡gracias!
Kyle
2
O incluso sin las dependencias de Spring cambiando el campo a acceso predeterminado (paquete protegido) para que sea simplemente accesible para la prueba.
Arne Burmeister el
22
Ejemplo:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier
44
Es posible que desee establecer estos campos por el constructor y luego mover la @Valueanotación al parámetro del constructor. Esto hace que el código de prueba sea mucho más simple al escribir el código manualmente, y Spring Boot no le importa.
Thorbjørn Ravn Andersen
Esta es la mejor respuesta para cambiar rápidamente una propiedad para un solo caso de prueba.
membersound
194

Desde Spring 4.1, puede configurar valores de propiedad solo en el código mediante el uso de org.springframework.test.context.TestPropertySourceanotaciones en el nivel de clase de Pruebas unitarias. Puede usar este enfoque incluso para inyectar propiedades en instancias de beans dependientes

Por ejemplo

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = FooTest.Config.class)
@TestPropertySource(properties = {
    "some.bar.value=testValue",
})
public class FooTest {

  @Value("${some.bar.value}")
  String bar;

  @Test
  public void testValueSetup() {
    assertEquals("testValue", bar);
  }


  @Configuration
  static class Config {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertiesResolver() {
        return new PropertySourcesPlaceholderConfigurer();
    }

  }

}

Nota: Es necesario tener una instancia org.springframework.context.support.PropertySourcesPlaceholderConfigureren contexto Spring

Editar 24-08-2017: Si está utilizando SpringBoot 1.4.0 y posterior, puede inicializar las pruebas @SpringBootTesty las @SpringBootConfigurationanotaciones. Más información aquí.

En el caso de SpringBoot tenemos el siguiente código

@SpringBootTest
@SpringBootConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
    "some.bar.value=testValue",
})
public class FooTest {

  @Value("${some.bar.value}")
  String bar;

  @Test
  public void testValueSetup() {
    assertEquals("testValue", bar);
  }

}
Dmytro Boichenko
fuente
3
Gracias, finalmente alguien respondió cómo anular el valor y no cómo establecer un campo. Derivo valores del campo de cadena en PostConstruct, por lo que necesito que Spring establezca el valor de cadena, no después de la construcción.
tequilacat
@Value ("$ aaaa"): ¿puede usar esta configuración interna de la clase misma?
Kalpesh Soni
No estoy seguro porque Config es una clase estática. Pero no dude en consultarlo
Dmytro Boichenko
¿Cómo puedo usar la anotación @Value en la clase Mockito Test?
user1575601
Estoy escribiendo una prueba de integración para un servicio que no hace referencia a ningún código que obtenga valores del archivo de propiedades, pero mi aplicación tiene una clase de configuración que está obteniendo valores del archivo de propiedades. Entonces, cuando estoy ejecutando la prueba, aparece un error de marcador de posición sin resolver, diga "$ {spring.redis.port}"
leyenda
63

No abuses de los campos privados que obtienes / establece por reflexión

Usar la reflexión como se hace en varias respuestas aquí es algo que podríamos evitar.
Aporta un pequeño valor aquí mientras presenta múltiples inconvenientes:

  • detectamos problemas de reflexión solo en tiempo de ejecución (ej .: campos que ya no existen)
  • Queremos encapsulación, pero no una clase opaca que oculte las dependencias que deberían ser visibles y que la clase sea más opaca y menos verificable.
  • Fomenta el mal diseño. Hoy declaras a @Value String field. Mañana puede declararlos 5o mencionarlos 10en esa clase y es posible que ni siquiera se dé cuenta de que disminuye el diseño de la clase. Con un enfoque más visible para establecer estos campos (como el constructor), lo pensará dos veces antes de agregar todos estos campos y probablemente los encapsulará en otra clase y los usará @ConfigurationProperties.

Haga que su clase sea comprobable tanto unitaria como en integración

Para poder escribir tanto pruebas unitarias simples (es decir, sin un contenedor de Spring en ejecución) como pruebas de integración para su clase de componente Spring, debe hacer que esta clase sea utilizable con o sin Spring.
Ejecutar un contenedor en una prueba unitaria cuando no es necesario es una mala práctica que ralentiza las compilaciones locales: no es necesario.
Agregué esta respuesta porque ninguna respuesta aquí parece mostrar esta distinción y, por lo tanto, dependen de un contenedor en ejecución sistemáticamente.

Así que creo que debería mover esta propiedad definida como interna de la clase:

@Component
public class Foo{   
    @Value("${property.value}") private String property;
    //...
}

en un parámetro constructor que será inyectado por Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Ejemplo de prueba unitaria

Puede crear instancias Foosin Spring e inyectar cualquier valor propertygracias al constructor:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Ejemplo de prueba de integración

Puede inyectar la propiedad en el contexto con Spring Boot de esta manera simple gracias al propertiesatributo de @SpringBootTest :

@SpringBootTest(properties="property.value=dummyValue")
public class FooTest{

   @Autowired
   Foo foo;

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

Podría usar como alternativa, @TestPropertySourcepero agrega una anotación adicional:

@SpringBootTest
@TestPropertySource("property.value=dummyValue")
public class FooTest{ ...}

Con Spring (sin Spring Boot), debería ser un poco más complicado, pero como no usé Spring sin Spring Boot desde hace mucho tiempo, no prefiero decir algo estúpido.

Como nota al margen: si tiene muchos @Valuecampos para establecer, extraerlos en una clase anotada @ConfigurationPropertieses más relevante porque no queremos un constructor con demasiados argumentos.

davidxxx
fuente
1
Gran respuesta. La mejor práctica aquí también es para los campos inicializados por el constructor final, es decirprivate String final property
kugo2006
1
Es bueno que alguien haya resaltado eso. Para que funcione solo con Spring, es necesario agregar la clase bajo prueba en @ContextConfiguration.
vimterd
53

Si lo desea, aún puede ejecutar sus pruebas dentro de Spring Context y establecer las propiedades requeridas dentro de la clase de configuración Spring. Si usa JUnit, use SpringJUnit4ClassRunner y defina una clase de configuración dedicada para sus pruebas así:

La clase bajo prueba:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

La clase de prueba:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(classes = SomeClassTestsConfig.class)
public class SomeClassTests {

    @Autowired
    private SomeClass someClass;

    @Autowired
    private SomeDependency someDependency;

    @Before
    public void setup() {
       Mockito.reset(someDependency);

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

Y la clase de configuración para esta prueba:

@Configuration
public class SomeClassTestsConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
        final PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
        Properties properties = new Properties();

        properties.setProperty("someProperty", "testValue");

        pspc.setProperties(properties);
        return pspc;
    }
    @Bean
    public SomeClass getSomeClass() {
        return new SomeClass();
    }

    @Bean
    public SomeDependency getSomeDependency() {
        // Mockito used here for mocking dependency
        return Mockito.mock(SomeDependency.class);
    }
}

Dicho esto, no recomendaría este enfoque, solo lo agregué aquí como referencia. En mi opinión, una forma mucho mejor es usar el corredor Mockito. En ese caso, no ejecuta pruebas dentro de Spring, lo cual es mucho más claro y simple.

Lukasz Korzybski
fuente
44
Estoy de acuerdo en que la mayoría de la lógica debe probarse con Mockito. Desearía que hubiera una mejor manera de probar la presencia y la corrección de las anotaciones que ejecutar pruebas en Spring.
Altair7852
29

Esto parece funcionar, aunque todavía es un poco detallado (me gustaría algo más corto aún):

@BeforeClass
public static void beforeClass() {
    System.setProperty("some.property", "<value>");
}

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
fuente
2
Creo que esta respuesta es más limpia, ya que es independiente de Spring, funciona bien para diferentes escenarios, como cuando tienes que usar corredores de prueba personalizados y no puedes simplemente agregar la @TestPropertyanotación.
raspacorp
Esto solo funciona para el enfoque de prueba de integración de Spring. Algunas respuestas y comentarios aquí se están inclinando hacia un enfoque de Mockito, para lo cual esto ciertamente no funciona (ya que no hay nada en Mockito que llene el @Values, independientemente de si la propiedad correspondiente está establecida o no.
Sander Verhagen
5

Agregar PropertyPlaceholderConfigurer en la configuración está funcionando para mí.

@Configuration
@ComponentScan
@EnableJpaRepositories
@EnableTransactionManagement
public class TestConfiguration {
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        builder.setType(EmbeddedDatabaseType.DERBY);
        return builder.build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setPackagesToScan(new String[] { "com.test.model" });
        // Use hibernate
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
        entityManagerFactoryBean.setJpaProperties(getHibernateProperties());
        return entityManagerFactoryBean;
    }

    private Properties getHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.show_sql", "false");
        properties.put("hibernate.dialect", "org.hibernate.dialect.DerbyDialect");
        properties.put("hibernate.hbm2ddl.auto", "update");
        return properties;
    }

    @Bean
    public JpaTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
         transactionManager.setEntityManagerFactory(
              entityManagerFactory().getObject()
         );

         return transactionManager;
    }

    @Bean
    PropertyPlaceholderConfigurer propConfig() {
        PropertyPlaceholderConfigurer placeholderConfigurer = new PropertyPlaceholderConfigurer();
        placeholderConfigurer.setLocation(new ClassPathResource("application_test.properties"));
        return placeholderConfigurer;
    }
}

Y en clase de prueba

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
public class DataServiceTest {

    @Autowired
    private DataService dataService;

    @Autowired
    private DataRepository dataRepository;

    @Value("${Api.url}")
    private String baseUrl;

    @Test
    public void testUpdateData() {
        List<Data> datas = (List<Data>) dataRepository.findAll();
        assertTrue(datas.isEmpty());
        dataService.updateDatas();
        datas = (List<Data>) dataRepository.findAll();
        assertFalse(datas.isEmpty());
    }
}
fjkjava
fuente