Preenchendo Spring @Value durante o teste de unidade

238

Estou tentando escrever um teste de unidade para um bean simples que é usado no meu programa para validar formulários. O bean é anotado com@Component e possui uma variável de classe que é inicializada usando

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

Gostaria de escrever testes de unidade para os métodos de validação dentro desta classe, no entanto, se possível, gostaria de fazê-lo sem utilizar o arquivo de propriedades. Meu raciocínio por trás disso é que, se o valor que estou retirando do arquivo de propriedades for alterado, gostaria que isso não afetasse meu caso de teste. Meu caso de teste está testando o código que valida o valor, não o valor em si.

Existe uma maneira de usar o código Java dentro da minha classe de teste para inicializar uma classe Java e preencher a propriedade Spring @Value dentro dessa classe e usá-la para testar?

Eu encontrei este tutorial que parece estar próximo, mas ainda usa um arquivo de propriedades. Prefiro que tudo seja código Java.

Kyle
fonte
Eu descrevi uma solução aqui para um problema semelhante. Espero que ajude.
horizon7 14/01

Respostas:

199

Se possível, eu tentaria escrever esses testes sem o Spring Context. Se você criar essa classe em seu teste sem mola, terá controle total sobre seus campos.

Para definir o @valuecampo, você pode usar Springs ReflectionTestUtils- ele possui um métodosetField para definir campos particulares.

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

Ralph
fonte
2
Exatamente o que eu estava tentando fazer e o que estava procurando para definir o valor dentro da minha classe, obrigado!
Kyle
2
Ou mesmo sem as dependências do Spring, alterando o campo para o acesso padrão (protegido por pacote) para torná-lo simplesmente acessível ao teste.
Arne Burmeister
22
Exemplo:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier
4
Você pode definir esses campos pelo construtor e depois mover a @Valueanotação para o parâmetro do construtor. Isso torna o código de teste muito mais simples ao escrever o código manualmente, e o Spring Boot não se importa.
Thorbjørn Ravn Andersen
Essa é a melhor resposta para alterar rapidamente uma propriedade para um único caso de teste.
membersound
194

Desde o Spring 4.1, você pode configurar valores de propriedade apenas no código usando org.springframework.test.context.TestPropertySource anotação no nível da classe Testes de Unidade. Você poderia usar essa abordagem mesmo para injetar propriedades em instâncias de bean dependentes

Por exemplo

@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: É necessário ter instância deorg.springframework.context.support.PropertySourcesPlaceholderConfigurer no contexto do Spring

Editar 24-08-2017: Se você estiver usando SpringBoot 1.4.0 e mais tarde você pode inicializar testes com @SpringBootTeste @SpringBootConfigurationanotações. Mais informações aqui

No caso do SpringBoot, temos o seguinte 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
fonte
3
Obrigado, finalmente alguém respondeu como substituir Value e não como definir um campo. Eu derivo valores do campo string no PostConstruct e, portanto, preciso que o valor da string seja definido pelo Spring, não após a construção.
18717 tequilacat 17/07
@Value ("$ aaaa") - você pode usar isso dentro da própria classe Config?
Kalpesh Soni
Não tenho certeza porque o Config é uma classe estática. Mas não hesite em verificar
Dmytro Boichenko
Como posso usar a anotação @Value na classe Mockito Test?
user1575601
Estou escrevendo um teste de integração para um serviço que não se refere a nenhum código que busca valores no arquivo de propriedades, mas meu aplicativo tem uma classe de configuração que está buscando valor no arquivo de propriedades. Portanto, quando estou executando o teste, ele está dando erro de espaço reservado não resolvido, diga "$ {spring.redis.port}"
legend
63

Não abuse dos campos particulares que são definidos por reflexão

Usar a reflexão como isso é feito em várias respostas aqui é algo que poderíamos evitar.
Ele traz um pequeno valor aqui, enquanto apresenta várias desvantagens:

  • detectamos problemas de reflexão apenas em tempo de execução (por exemplo: campos que não existem mais)
  • Queremos um encapsulamento, mas não uma classe opaca que oculte dependências que devem ser visíveis e torne a classe mais opaca e menos testável.
  • incentiva o mau design. Hoje você declara a @Value String field. Amanhã, você poderá declarar 5ou 10participar dessa classe e talvez nem esteja ciente de que diminui o design da classe. Com uma abordagem mais visível para definir esses campos (como construtor), você pensará duas vezes antes de adicionar todos esses campos e provavelmente os encapsulará em outra classe e uso @ConfigurationProperties.

Torne sua classe testável, tanto unitária quanto em integração

Para poder escrever testes de unidade simples (sem um contêiner de mola em execução) e testes de integração para sua classe de componente Spring, você deve tornar essa classe utilizável com ou sem Spring.
Executar um contêiner em um teste de unidade quando não é necessário é uma prática ruim que diminui as compilações locais: você não deseja isso.
Eu adicionei esta resposta porque nenhuma resposta aqui parece mostrar essa distinção e, portanto, eles dependem de um contêiner em execução sistematicamente.

Então, acho que você deve mover essa propriedade definida como uma parte interna da classe:

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

em um parâmetro construtor que será injetado pelo Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Exemplo de teste de unidade

Você pode instanciar Foosem Spring e injetar qualquer valor propertydevido ao construtor:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Exemplo de Teste de Integração

Você pode injetar a propriedade no contexto com o Spring Boot desta maneira simples, graças ao propertiesatributo de @SpringBootTest :

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

   @Autowired
   Foo foo;

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

Você pode usar como alternativa, @TestPropertySourcemas ele adiciona uma anotação adicional:

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

Com o Spring (sem o Spring Boot), deve ser um pouco mais complicado, mas como não uso o Spring sem o Spring Boot há muito tempo, não prefiro dizer uma coisa estúpida.

Como uma observação lateral: se você tiver muitos @Valuecampos para definir, extraí-los em uma classe anotada @ConfigurationPropertiesé mais relevante porque não queremos um construtor com muitos argumentos.

davidxxx
fonte
1
Ótima resposta. A melhor prática aqui também é para campos inicializados pelo construtor final, ou seja,private String final property
kugo2006 21/04
1
É bom que alguém tenha destacado isso. Para fazê-lo funcionar apenas com o Spring, é necessário adicionar a classe em teste no @ContextConfiguration.
vimterd 03/06
53

Se desejar, você ainda pode executar seus testes no Spring Context e definir as propriedades necessárias na classe de configuração do Spring. Se você usa JUnit, use SpringJUnit4ClassRunner e defina a classe de configuração dedicada para seus testes assim:

A classe em teste:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

A classe de teste:

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

E a classe de configuração para este teste:

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

Dito isto, eu não recomendaria essa abordagem, apenas a adicionei aqui para referência. Na minha opinião, a melhor maneira é usar o corredor Mockito. Nesse caso, você não executa testes no Spring, o que é muito mais claro e mais simples.

Lukasz Korzybski
fonte
4
Concordo que a maior parte da lógica deve ser testada com o Mockito. Eu gostaria que houvesse uma maneira melhor de testar a presença e a correção das anotações do que executar testes no Spring.
precisa saber é o seguinte
29

Isso parece funcionar, embora ainda seja um pouco detalhado (eu gostaria de algo mais curto ainda):

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
fonte
2
Eu acho que essa resposta é mais limpa, pois é independente da primavera, funciona bem em diferentes cenários, como quando você precisa usar testes de execução personalizados e não pode simplesmente adicionar a @TestPropertyanotação.
Raspacorp 23/02/19
Isso funciona apenas para a abordagem de teste de integração do Spring. Algumas respostas e comentários aqui estão inclinados para uma abordagem do Mockito, para a qual isso certamente não funciona (já que não há nada no Mockito que preencha os @Values, independentemente de a propriedade correspondente estar configurada ou não.
Sander Verhagen
5

Adicionar PropertyPlaceholderConfigurer na configuração está funcionando para mim.

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

E na aula de teste

@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
fonte