Wypełnianie wiosny @ Wartość podczas testu jednostkowego

238

Próbuję napisać test jednostkowy dla prostej fasoli, która jest używana w moim programie do sprawdzania poprawności formularzy. Fasola jest opatrzona adnotacjami @Componenti ma zmienną klasy, która jest inicjowana za pomocą

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

Chciałbym napisać testy jednostkowe dla metod sprawdzania poprawności w tej klasie, jednak jeśli to możliwe, chciałbym to zrobić bez korzystania z pliku właściwości. Moje uzasadnienie jest takie, że jeśli wartość, którą pobieram z pliku właściwości, zmienia się, chciałbym, aby nie miało to wpływu na mój przypadek testowy. Mój przypadek testowy testuje kod, który sprawdza wartość, a nie samą wartość.

Czy istnieje sposób użycia kodu Java w mojej klasie testowej do zainicjowania klasy Java i wypełnienia właściwości Spring @Value w tej klasie, a następnie użycia jej do przetestowania?

Znalazłem ten poradnik, który wydaje się być blisko, ale nadal używa pliku właściwości. Wolałbym, żeby to wszystko był kod Java.

Kyle
źródło
Opisałem tutaj rozwiązanie podobnego problemu. Mam nadzieję, że to pomoże.
horizon7

Odpowiedzi:

199

Jeśli to możliwe, spróbuję napisać te testy bez kontekstu wiosny. Jeśli utworzysz tę klasę w teście bez wiosny, masz pełną kontrolę nad jej polami.

Aby ustawić @valuepole, możesz użyć Sprężyn ReflectionTestUtils- ma metodę setFieldustawiania pól prywatnych.

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

Ralph
źródło
2
Dokładnie to, co próbowałem zrobić i czego szukałem, aby ustawić wartość w mojej klasie, dzięki!
Kyle
2
Lub nawet bez zależności Spring, zmieniając pole na domyślny dostęp (chroniony pakietem), aby był on po prostu dostępny dla testu.
Arne Burmeister
22
Przykład:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier,
4
Możesz ustawić te pola w ustawieniach konstruktora, a następnie przenieść @Valueadnotację do parametru konstruktora. To znacznie upraszcza kod testowy podczas ręcznego pisania kodu, a Spring Boot nie dba o to.
Thorbjørn Ravn Andersen
To najlepsza odpowiedź, aby po prostu szybko zmienić jedną właściwość dla pojedynczej skrzynki testowej.
Membersound
194

Od wiosny 4.1 można ustawić wartości właściwości tylko w kodzie, używając org.springframework.test.context.TestPropertySourceadnotacji na poziomie klasy testów jednostkowych. Można użyć tego podejścia nawet do wstrzykiwania właściwości do zależnych instancji komponentu bean

Na przykład

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

  }

}

Uwaga: Konieczne jest posiadanie instancji org.springframework.context.support.PropertySourcesPlaceholderConfigurerw kontekście wiosennym

Edytuj 24-08-2017: Jeśli używasz SpringBoot 1.4.0 i nowszych, możesz zainicjować testy za pomocą @SpringBootTesti @SpringBootConfigurationadnotacji. Więcej informacji tutaj

W przypadku SpringBoot mamy następujący kod

@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
źródło
3
Dziękujemy, w końcu ktoś odpowiedział, jak zastąpić wartość, a nie jak ustawić pole. Czerpię wartości z pola ciągu w PostConstruct, dlatego potrzebuję ustawić wartość ciągu Spring, a nie po zakończeniu budowy.
tequilacat 17.07.17
@Value („$ aaaa”) - czy możesz użyć tego samego Config wewnątrz klasy?
Kalpesh Soni
Nie jestem pewien, ponieważ Config jest klasą statyczną. Ale proszę sprawdzić
Dmytro Boichenko
Jak korzystać z adnotacji @Value w klasie testowej Mockito?
user1575601,
Piszę test integracji dla usługi, która nie odwołuje żadnego kodu, który pobiera wartości z pliku właściwości, ale moja aplikacja ma klasę konfiguracji, która pobiera wartość z pliku właściwości. Kiedy uruchamiam test, pojawia się błąd nierozwiązanego symbolu zastępczego, powiedz „$ {spring.redis.port}”
legenda
63

Nie nadużywaj prywatnych pól, które zostaną ustawione / ustawione przez odbicie

Używanie refleksji tak, jak ma to miejsce w kilku odpowiedziach, jest czymś, czego możemy uniknąć.
Przynosi tutaj małą wartość, a jednocześnie ma wiele wad:

  • wykrywamy problemy z odbiciem tylko w czasie wykonywania (np. pola już nie istnieją)
  • Chcemy enkapsulacji, ale nie nieprzezroczystej klasy, która ukrywa zależności, które powinny być widoczne, a klasa bardziej nieprzejrzysta i mniej testowalna.
  • zachęca do złego projektu. Dziś deklarujesz @Value String field. Jutro możesz zadeklarować 5lub jedną 10z nich w tej klasie i możesz nawet nie być świadomy, że zmniejszysz projekt klasy. Przy bardziej widocznym podejściu do ustawiania tych pól (takich jak konstruktor), pomyślisz dwa razy przed dodaniem wszystkich tych pól i prawdopodobnie zamkniesz je w innej klasie i użyjesz @ConfigurationProperties.

Spraw, by Twoja klasa była testowalna zarówno jako jednolita, jak i integracyjna

Aby móc pisać zarówno zwykłe testy jednostkowe (bez działającego kontenera sprężynowego), jak i testy integracyjne dla klasy komponentów Spring, musisz uczynić tę klasę użyteczną ze sprężyną lub bez niej.
Uruchamianie kontenera w teście jednostkowym, gdy nie jest to wymagane, jest złą praktyką, która spowalnia lokalne kompilacje: nie chcesz tego.
Dodałem tę odpowiedź, ponieważ żadna odpowiedź tutaj nie wydaje się wykazywać tego rozróżnienia, więc polegają one systematycznie na działającym kontenerze.

Myślę więc, że powinieneś przenieść tę właściwość zdefiniowaną jako wewnętrzna klasy:

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

do parametru konstruktora, który zostanie wstrzyknięty przez Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Przykład testu jednostkowego

Możesz utworzyć instancję Foobez Springa i wstrzyknąć dowolną wartość propertydzięki Konstruktorowi:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Przykład testu integracji

Możesz wstrzyknąć właściwość w kontekście za pomocą Spring Boot w ten prosty sposób dzięki propertiesatrybutowi @SpringBootTest :

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

   @Autowired
   Foo foo;

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

Możesz użyć jako alternatywy, @TestPropertySourceale dodaje to dodatkową adnotację:

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

W przypadku Spring (bez Spring Boot) powinno to być trochę bardziej skomplikowane, ale ponieważ od dłuższego czasu nie używałem Spring bez Spring Boot, nie wolę mówić głupio.

Na marginesie: jeśli masz wiele @Valuepól do ustawienia, wyodrębnienie ich do klasy opatrzonej adnotacjami @ConfigurationPropertiesjest bardziej odpowiednie, ponieważ nie chcemy konstruktora z zbyt dużą liczbą argumentów.

davidxxx
źródło
1
Świetna odpowiedź. Najlepszą praktyką jest także inicjowanie przez konstruktora pól final, tj.private String final property
kugo2006
1
Fajnie, że ktoś to podkreślił. Aby działał tylko z Springem, konieczne jest dodanie testowanej klasy w @ContextConfiguration.
vimterd
53

Jeśli chcesz, możesz nadal uruchamiać testy w ramach Spring Context i ustawiać wymagane właściwości w klasie Spring. Jeśli używasz JUnit, użyj SpringJUnit4ClassRunner i zdefiniuj dedykowaną klasę konfiguracji dla swoich testów w ten sposób:

Testowana klasa:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

Klasa testowa:

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

I klasa konfiguracji dla tego testu:

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

Powiedziawszy to, nie zalecałbym tego podejścia, po prostu dodałem go tutaj w celach informacyjnych. Moim zdaniem znacznie lepszym sposobem jest użycie biegacza Mockito. W takim przypadku nie uruchamiasz testów wewnątrz Springa, co jest znacznie bardziej przejrzyste i prostsze.

Łukasz Korzybski
źródło
4
Zgadzam się, że większość logiki powinna zostać przetestowana za pomocą Mockito. Chciałbym, aby istniał lepszy sposób testowania obecności i poprawności adnotacji niż przeprowadzanie testów przez Spring.
Altair7852
29

Wydaje się, że to działa, choć wciąż jest trochę gadatliwe (chciałbym jeszcze coś krótszego):

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
źródło
2
Myślę, że ta odpowiedź jest czystsza, ponieważ jest wiosenna agnostyczna, działa dobrze w różnych scenariuszach, na przykład gdy musisz użyć niestandardowych testów i nie możesz po prostu dodać @TestPropertyadnotacji.
raspacorp
Działa to tylko w przypadku testu integracji wiosennej. Niektóre odpowiedzi i komentarze tutaj skłaniają się ku podejściu Mockito, dla którego to na pewno nie działa (ponieważ w Mockito nie ma nic, co zapełniłoby te @Value, niezależnie od tego, czy odpowiednia właściwość jest ustawiona, czy nie.
Sander Verhagen
5

Dodanie PropertyPlaceholderConfigurer do konfiguracji działa dla mnie.

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

I w klasie testowej

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