Auffüllen von Spring @Value während des Unit-Tests

238

Ich versuche, einen Unit-Test für eine einfache Bean zu schreiben, die in meinem Programm zum Validieren von Formularen verwendet wird. Die Bean ist mit Anmerkungen versehen @Componentund verfügt über eine Klassenvariable, die mit initialisiert wird

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

Ich möchte Unit-Tests für die Validierungsmethoden in dieser Klasse schreiben, möchte dies jedoch nach Möglichkeit ohne Verwendung der Eigenschaftendatei tun. Meine Argumentation dahinter ist, dass wenn sich der Wert, den ich aus der Eigenschaftendatei ziehe, ändert, ich möchte, dass dies meinen Testfall nicht beeinflusst. Mein Testfall testet den Code, der den Wert validiert, nicht den Wert selbst.

Gibt es eine Möglichkeit, Java-Code in meiner Testklasse zu verwenden, um eine Java-Klasse zu initialisieren und die Spring @ Value-Eigenschaft in dieser Klasse zu füllen, um sie dann zum Testen zu verwenden?

Ich habe dieses How To gefunden , das nah zu sein scheint, aber immer noch eine Eigenschaftendatei verwendet. Ich würde lieber alles Java-Code sein.

Kyle
quelle
Ich habe hier eine Lösung für ein ähnliches Problem beschrieben. Ich hoffe es hilft.
Horizont7

Antworten:

199

Wenn möglich, würde ich versuchen, diesen Test ohne Spring Context zu schreiben. Wenn Sie diese Klasse in Ihrem Test ohne Feder erstellen, haben Sie die volle Kontrolle über ihre Felder.

Um das @valueFeld festzulegen, können Sie Federn verwenden. ReflectionTestUtilsEs gibt eine Methode setFieldzum Festlegen privater Felder.

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

Ralph
quelle
2
Genau das, was ich versucht habe und wonach ich gesucht habe, um den Wert in meiner Klasse festzulegen, danke!
Kyle
2
Oder auch ohne Spring-Abhängigkeiten, indem Sie das Feld auf Standardzugriff (paketgeschützt) ändern, um es für den Test einfach zugänglich zu machen.
Arne Burmeister
22
Beispiel:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier
4
Möglicherweise möchten Sie diese Felder vom Konstruktor festlegen und dann die @ValueAnmerkung in den Konstruktorparameter verschieben. Dies macht den Testcode viel einfacher, wenn der Code manuell geschrieben wird, und Spring Boot ist das egal.
Thorbjørn Ravn Andersen
Dies ist die beste Antwort, um schnell eine Eigenschaft für einen einzelnen Testfall zu ändern.
Membersound
194

Seit Spring 4.1 können Sie Eigenschaftswerte nur im Code mithilfe von org.springframework.test.context.TestPropertySourceAnmerkungen auf Klassenebene für Komponententests einrichten . Sie können diesen Ansatz auch zum Einfügen von Eigenschaften in abhängige Bean-Instanzen verwenden

Beispielsweise

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

  }

}

Hinweis: Es ist erforderlich, eine Instanz org.springframework.context.support.PropertySourcesPlaceholderConfigurerim Spring-Kontext zu haben

Bearbeiten 24-08-2017: Wenn Sie SpringBoot 1.4.0 und höher verwenden, können Sie Tests mit @SpringBootTestund @SpringBootConfigurationAnmerkungen initialisieren . Mehr Infos hier

Im Falle von SpringBoot haben wir folgenden Code

@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
quelle
3
Vielen Dank, endlich hat jemand geantwortet, wie man den Wert überschreibt und nicht wie man ein Feld setzt. Ich leite Werte aus dem Zeichenfolgenfeld in PostConstruct ab und muss daher den Zeichenfolgenwert von Spring und nicht nach der Erstellung festlegen.
Tequilacat
@Value ("$ aaaa") - können Sie dies innerhalb der Klasse Config selbst verwenden?
Kalpesh Soni
Ich bin mir nicht sicher, da Config eine statische Klasse ist. Aber bitte
zögern
Wie kann ich die @ Value-Annotation in der Mockito-Testklasse verwenden?
user1575601
Ich schreibe einen Integrationstest für einen Dienst, der keinen Code referenziert, der Werte aus der Eigenschaftendatei abruft, aber meine Anwendung verfügt über eine Konfigurationsklasse, die Werte aus der Eigenschaftendatei abruft. Wenn ich also einen Test durchführe, wird ein Fehler beim Auflösen des Platzhalters angezeigt. Sagen Sie "$ {spring.redis.port}"
Legende
61

Missbrauche keine privaten Felder, die durch Reflexion erhalten / gesetzt werden

Die Verwendung von Reflexion, wie sie in mehreren Antworten hier erfolgt, ist etwas, das wir vermeiden könnten.
Es bringt hier einen kleinen Wert, während es mehrere Nachteile aufweist:

  • Wir erkennen Reflexionsprobleme nur zur Laufzeit (z. B. Felder, die nicht mehr vorhanden sind).
  • Wir wollen die Kapselung, aber keine undurchsichtige Klasse, die Abhängigkeiten verbirgt, die sichtbar sein sollten, und die Klasse undurchsichtiger und weniger testbar macht.
  • es fördert schlechtes Design. Heute erklären Sie a @Value String field. Morgen können Sie 5oder 10von ihnen in dieser Klasse deklarieren , und Sie sind sich möglicherweise nicht einmal bewusst, dass Sie das Design der Klasse verringern. Mit einem besser sichtbaren Ansatz zum Festlegen dieser Felder (z. B. Konstruktor) werden Sie zweimal überlegen, bevor Sie alle diese Felder hinzufügen, und Sie werden sie wahrscheinlich in eine andere Klasse einkapseln und verwenden @ConfigurationProperties.

Machen Sie Ihre Klasse sowohl einheitlich als auch in Integration testbar

Um sowohl einfache Komponententests (dh ohne laufenden Federbehälter) als auch Integrationstests für Ihre Spring-Komponentenklasse schreiben zu können, müssen Sie diese Klasse mit oder ohne Spring verwendbar machen.
Das Ausführen eines Containers in einem Komponententest, wenn dies nicht erforderlich ist, ist eine schlechte Vorgehensweise, die lokale Builds verlangsamt: Das möchten Sie nicht.
Ich habe diese Antwort hinzugefügt, da keine Antwort hier diese Unterscheidung zu zeigen scheint und sie sich daher systematisch auf einen laufenden Container stützen.

Daher denke ich, dass Sie diese Eigenschaft verschieben sollten, die als intern innerhalb der Klasse definiert ist:

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

in einen Konstruktorparameter, der von Spring eingefügt wird:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Unit Test Beispiel

Sie können instanziieren Foo ohne Spring und propertydank des Konstruktors einen beliebigen Wert einfügen:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Integrationstestbeispiel

Sie können die Eigenschaft auf diese einfache Weise im Kontext von Spring Boot einfügen, dank der properties Attributs @SpringBootTest :

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

   @Autowired
   Foo foo;

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

Sie könnten als Alternative verwenden @TestPropertySource , es wird jedoch eine zusätzliche Anmerkung hinzugefügt:

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

Mit Spring (ohne Spring Boot) sollte es etwas komplizierter sein, aber da ich Spring ohne Spring Boot schon lange nicht mehr verwendet habe, möchte ich nicht lieber etwas Dummes sagen.

Als Randnotiz: Wenn Sie viele @ValueFelder festlegen müssen, ist das Extrahieren in eine mit Anmerkungen versehene Klasse @ConfigurationPropertiesrelevanter, da wir keinen Konstruktor mit zu vielen Argumenten möchten.

davidxxx
quelle
Gute Antwort. Best Practice hier ist auch für vom Konstruktor initialisierte Felder final, dhprivate String final property
kugo2006
1
Es ist schön, dass jemand das hervorgehoben hat. Damit es nur mit Spring funktioniert, muss die zu testende Klasse in @ContextConfiguration hinzugefügt werden.
Vimterd
53

Wenn Sie möchten, können Sie Ihre Tests weiterhin in Spring Context ausführen und die erforderlichen Eigenschaften in der Spring-Konfigurationsklasse festlegen. Wenn Sie JUnit verwenden, verwenden Sie SpringJUnit4ClassRunner und definieren Sie eine dedizierte Konfigurationsklasse für Ihre Tests wie folgt:

Die zu testende Klasse:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

Die Testklasse:

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

Und die Konfigurationsklasse für diesen Test:

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

Trotzdem würde ich diesen Ansatz nicht empfehlen, ich habe ihn hier nur als Referenz hinzugefügt. Meiner Meinung nach ist es viel besser, Mockito Runner zu verwenden. In diesem Fall führen Sie in Spring überhaupt keine Tests durch, was viel klarer und einfacher ist.

Lukasz Korzybski
quelle
4
Ich bin damit einverstanden, dass die meiste Logik mit Mockito getestet werden sollte. Ich wünschte, es gäbe eine bessere Möglichkeit, das Vorhandensein und die Richtigkeit von Anmerkungen zu testen, als Tests über Spring durchzuführen.
Altair7852
29

Dies scheint zu funktionieren, obwohl es immer noch etwas ausführlich ist (ich hätte gerne noch etwas kürzeres):

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
quelle
2
Ich denke, diese Antwort ist sauberer, da sie frühlingsunabhängig ist. Sie funktioniert gut für verschiedene Szenarien, z. B. wenn Sie benutzerdefinierte Testläufer verwenden müssen und nicht einfach die @TestPropertyAnmerkung hinzufügen können .
Raspacorp
Dies funktioniert nur für den Spring-Integrationstestansatz. Einige Antworten und Kommentare hier neigen zu einem Mockito-Ansatz, für den dies sicherlich nicht funktioniert (da es in Mockito nichts gibt, das das @Values bevölkert , unabhängig davon, ob die entsprechende Eigenschaft festgelegt ist oder nicht.
Sander Verhagen
5

Das Hinzufügen von PropertyPlaceholderConfigurer zur Konfiguration funktioniert für mich.

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

Und in der Testklasse

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