Jak przetestować repozytoria Spring Data?

136

Chcę mieć repozytorium (powiedzmy UserRepository) utworzone przy pomocy Spring Data. Jestem nowy w wiosennych danych (ale nie na wiosnę) i używam tego samouczka . Wybrane przeze mnie technologie obsługi bazy danych to JPA 2.1 i Hibernate. Problem w tym, że nie mam pojęcia, jak napisać testy jednostkowe dla takiego repozytorium.

Weźmy create()na przykład metodę. Ponieważ pracuję najpierw test, mam napisać dla niego test jednostkowy - i tam napotykam trzy problemy:

  • Po pierwsze, w jaki sposób mogę wstrzyknąć makietę an EntityManagerdo nieistniejącej implementacji UserRepositoryinterfejsu? Spring Data wygenerowałoby implementację opartą na tym interfejsie:

    public interface UserRepository extends CrudRepository<User, Long> {}

    Nie wiem jednak, jak zmusić go do użycia EntityManagermocka i innych mocków - gdybym sam napisał implementację, prawdopodobnie miałbym metodę ustawiającą dla EntityManager, która pozwoliłaby mi użyć mojego mocka do testu jednostkowego. (Jeśli chodzi o rzeczywistą łączność z bazami danych, mam JpaConfigurationklasę, z uwagami @Configurationi @EnableJpaRepositories, który programowo określa fasola DataSource, EntityManagerFactory, EntityManageritd. - ale repozytoria powinny być testy w obsłudze i pozwala na nadpisanie tych rzeczy).

  • Po drugie, czy powinienem przetestować interakcje? Trudno mi się domyślić, jakie metody EntityManageri jakie Querymają być wywoływane (podobnie verify(entityManager).createNamedQuery(anyString()).getResultList();), ponieważ to nie ja piszę implementację.

  • Po trzecie, czy mam najpierw przetestować jednostkowo metody wygenerowane przez Spring-Data? Jak wiem, kod biblioteki innej firmy nie powinien być testowany jednostkowo - tylko kod, który piszą sami programiści, ma być testowany jednostkowo. Ale jeśli to prawda, nadal sprowadza pierwsze pytanie z powrotem na scenę: powiedzmy, mam kilka niestandardowych metod dla mojego repozytorium, dla którego będę pisać implementację, jak wstrzyknąć moje makiety EntityManageri Querydo ostatecznego, wygenerowanego magazyn?

Uwaga: będę testował moje repozytoria przy użyciu zarówno integracji, jak i testów jednostkowych. Do testów integracji używam bazy danych HSQL w pamięci i oczywiście nie używam bazy danych do testów jednostkowych.

I prawdopodobnie czwarte pytanie, czy poprawne jest testowanie poprawności tworzenia grafów obiektów i ich pobierania w testach integracji (powiedzmy, mam złożony graf obiektów zdefiniowany w Hibernate)?

Aktualizacja: dzisiaj kontynuowałem eksperymentowanie z próbnym wstrzyknięciem - stworzyłem statyczną klasę wewnętrzną, aby umożliwić próbny wtrysk.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class UserRepositoryTest {

@Configuration
@EnableJpaRepositories(basePackages = "com.anything.repository")
static class TestConfiguration {

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        return mock(EntityManagerFactory.class);
    }

    @Bean
    public EntityManager entityManager() {
        EntityManager entityManagerMock = mock(EntityManager.class);
        //when(entityManagerMock.getMetamodel()).thenReturn(mock(Metamodel.class));
        when(entityManagerMock.getMetamodel()).thenReturn(mock(MetamodelImpl.class));
        return entityManagerMock;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return mock(JpaTransactionManager.class);
    }

}

@Autowired
private UserRepository userRepository;

@Autowired
private EntityManager entityManager;

@Test
public void shouldSaveUser() {
    User user = new UserBuilder().build();
    userRepository.save(user);
    verify(entityManager.createNamedQuery(anyString()).executeUpdate());
}

}

Jednak uruchomienie tego testu daje mi następujący ślad stosu:

java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:101)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:319)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:212)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175)
at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository': Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1493)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1197)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:684)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:121)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
    at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:100)
    at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:250)
    at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContextInternal(CacheAwareContextLoaderDelegate.java:64)
    at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:91)
    ... 28 more
Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:108)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:62)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1489)
    ... 44 more
user1797032
źródło

Odpowiedzi:

118

tl; dr

Krótko mówiąc - nie ma możliwości przeprowadzenia testów jednostkowych repozytoriów JPA Spring Data z prostego powodu: mockowanie wszystkich części API JPA, które wywołujemy w celu załadowania repozytoriów, jest zbyt kłopotliwe. Testy jednostkowe i tak nie mają tu zbytniego sensu, ponieważ zwykle nie piszesz samodzielnie żadnego kodu implementacji (zobacz poniższy akapit o niestandardowych implementacjach), więc testowanie integracyjne jest najbardziej rozsądnym podejściem.

Detale

Wykonujemy sporo wstępnej weryfikacji i konfiguracji, aby upewnić się, że możesz załadować tylko aplikację, która nie ma nieprawidłowych zapytań pochodnych itp.

  • Tworzymy i buforujemy CriteriaQuerywystąpienia dla zapytań pochodnych, aby upewnić się, że metody zapytań nie zawierają żadnych literówek. Wymaga to pracy z interfejsem Criteria API, a także z meta.model.
  • Weryfikujemy ręcznie zdefiniowane zapytania, prosząc EntityManagero utworzenie Queryinstancji dla tych zapytań (co skutecznie uruchamia walidację składni zapytania).
  • Sprawdzamy pod Metamodelkątem metadanych o obsługiwanych typach domen, aby przygotować sprawdzenie, czy są nowe itp.

Wszystkie rzeczy, które prawdopodobnie odłożyłbyś w ręcznie napisanym repozytorium, co może spowodować awarię aplikacji w czasie wykonywania (z powodu nieprawidłowych zapytań itp.).

Jeśli się nad tym zastanowić, nie ma kodu, który piszesz dla swoich repozytoriów, więc nie ma potrzeby pisania żadnych testów jednostkowych . Po prostu nie ma potrzeby, aby jak można polegać na naszej bazie testowej złapać podstawowe błędy (jeśli jeszcze się zdarzyć, aby uruchomić w jeden, nie krępuj się podnieść bilet ). Jednak zdecydowanie potrzebne są testy integracyjne, aby przetestować dwa aspekty warstwy trwałości, ponieważ są to aspekty związane z Twoją domeną:

  • mapowania jednostek
  • semantyka zapytań (składnia i tak jest weryfikowana przy każdej próbie ładowania początkowego).

Testy integracyjne

Odbywa się to zwykle przy użyciu bazy danych w pamięci i przypadków testowych, które ładują Springa ApplicationContextzwykle za pomocą struktury kontekstu testowego (tak jak już to robisz), wstępnie wypełniają bazę danych (wstawiając instancje obiektów za pośrednictwem EntityManagerrepozytorium lub lub zwykłego SQL), a następnie wykonaj metody zapytania, aby zweryfikować ich wynik.

Testowanie niestandardowych wdrożeń

Niestandardowe części implementacyjne repozytorium są napisane w taki sposób , że nie muszą wiedzieć o Spring Data JPA. Są to zwykłe wiosenne ziarna, które są EntityManagerwstrzykiwane. Można oczywiście chce spróbować drwić interakcje z nim, ale szczerze mówiąc, jednostka testowania WZP nie było zbyt przyjemne doświadczenie dla nas, jak to działa z dość dużą ilością indirections ( EntityManager-> CriteriaBuilder, CriteriaQueryitd.), Tak że kończysz z wyśmiewaniem, zwracaniem kpiny i tak dalej.

Oliver Drotbohm
źródło
5
Czy masz link do małego przykładu testu integracji z bazą danych w pamięci (np. H2)?
Wim Deblauwe
7
Przykłady tu użyć hsqldb. Przejście na H2 jest w zasadzie kwestią zamiany zależności w pom.xml.
Oliver Drotbohm
3
Dzięki, ale miałem nadzieję zobaczyć przykład, który wstępnie wypełnia bazę danych i / lub naprawdę sprawdza bazę danych.
Wim Deblauwe
1
Link znajdujący się za „napisanym w jakiś sposób” już nie działa. Może możesz to zaktualizować?
Wim Deblauwe
1
Czy więc proponujesz użycie testów integracyjnych zamiast testów jednostkowych również w przypadku niestandardowych implementacji? I w ogóle nie pisać dla nich testów jednostkowych? Aby wyjaśnic. W porządku, jeśli tak. Rozumiem powód (zbyt skomplikowany, by kpić ze wszystkich rzeczy). Jestem nowy w testowaniu JPA, więc chcę to rozgryźć.
Ruslan Stelmachenko
48

Dzięki Spring Boot + Spring Data stało się to całkiem proste:

@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {

    @Autowired
    MyRepository subject;

    @Test
    public void myTest() throws Exception {
        subject.save(new MyEntity());
    }
}

Rozwiązanie @heez ukazuje pełny kontekst, a to pokazuje tylko to, co jest potrzebne do działania JPA + Transaction. Zauważ, że powyższe rozwiązanie spowoduje wyświetlenie testowej bazy danych w pamięci, biorąc pod uwagę, że można ją znaleźć w ścieżce klas.

Markus T.
źródło
7
To jest test integracyjny , a nie test jednostkowy, o którym wspominał OP
Iwo Kucharski
16
@IwoKucharski. Masz rację co do terminologii. Jednak: biorąc pod uwagę, że Spring Data implementuje interfejs za Ciebie, korzystanie ze Springa jest trudne i od tego momentu staje się to testem integracji. Gdybym zadał takie pytanie, prawdopodobnie poprosiłbym również o test jednostkowy, nie myśląc o terminologii. Dlatego nie uważałem tego za główny, a nawet centralny punkt pytania.
Markus T
@RunWith(SpringRuner.class)jest już uwzględniony w @DataJpaTest.
Maroun
@IwoKucharski, dlaczego to jest test integracji, a nie test jednostkowy?
user1182625
@ user1182625 @RunWith(SpringRunner.classuruchamia kontekst sprężynowy, co oznacza, że ​​sprawdza integrację między kilkoma jednostkami. Test jednostkowy to testowanie pojedynczej jednostki -> pojedynczej klasy. Następnie piszesz MyClass sut = new MyClass();i testujesz obiekt sut (sut = usługa w trakcie testu)
Iwo Kucharski
21

To może przyjść trochę za późno, ale napisałem coś właśnie w tym celu. Moja biblioteka wyśmieje dla ciebie podstawowe metody repozytorium crud, a także zinterpretuje większość funkcji twoich metod zapytań. Będziesz musiał wprowadzić funkcje do własnych zapytań natywnych, ale reszta zostanie wykonana za Ciebie.

Spójrz:

https://github.com/mmnaseri/spring-data-mock

AKTUALIZACJA

To jest teraz w centrum Maven iw całkiem niezłym stanie.

Milad Naseri
źródło
16

Jeśli używasz Spring Boota, możesz po prostu użyć go @SpringBootTestdo załadowania swojego ApplicationContext(o czym szczeka na ciebie twój ślad stosu). Pozwala to na automatyczne podłączanie do repozytoriów danych wiosennych. Pamiętaj, aby dodać, @RunWith(SpringRunner.class)aby wybrać adnotacje specyficzne dla wiosny:

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrphanManagementTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  public void saveTest() {
    User user = new User("Tom");
    userRepository.save(user);
    Assert.assertNotNull(userRepository.findOne("Tom"));
  }
}

Możesz przeczytać więcej o testowaniu w wiosennym rozruchu w ich dokumentacji .

heez
źródło
To całkiem dobry przykład, ale moim zdaniem uproszczony. Czy są sytuacje, w których ten test może się nawet nie powieść?
HopeKing
Nie ten jako taki, ale przypuśćmy, że chcesz przetestować Predicates (co było moim przypadkiem użycia), działa całkiem dobrze.
heez
1
dla mnie repozytorium jest zawsze puste. Jakaś pomoc?
Atul Chaudhary
To jest najlepsza odpowiedź. W ten sposób testujesz CrudRepo, Encję i skrypty DDL, które tworzą tabele Encji.
MirandaVeracruzDeLaHoyaCardina
Napisałem test dokładnie taki jak ten. Działa doskonale, gdy implementacja Repozytorium wykorzystuje jdbcTemplate. Jednak kiedy zmieniam implementację dla spring-data (poprzez rozszerzenie interfejsu z Repository), test kończy się niepowodzeniem i userRepository.findOne zwraca wartość null. Jakieś pomysły, jak to rozwiązać?
Rega
8

W ostatniej wersji Spring Boot 2.1.1.RELEASE jest to proste:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SampleApplication.class)
public class CustomerRepositoryIntegrationTest {

    @Autowired
    CustomerRepository repository;

    @Test
    public void myTest() throws Exception {

        Customer customer = new Customer();
        customer.setId(100l);
        customer.setFirstName("John");
        customer.setLastName("Wick");

        repository.save(customer);

        List<?> queryResult = repository.findByLastName("Wick");

        assertFalse(queryResult.isEmpty());
        assertNotNull(queryResult.get(0));
    }
}

Kompletny kod:

https://github.com/jrichardsz/spring-boot-templates/blob/master/003-hql-database-with-integration-test/src/test/java/test/CustomerRepositoryIntegrationTest.java

JRichardsz
źródło
3
To raczej niekompletny „przykład”: nie można go zbudować, testy „integracyjne” używają tej samej konfiguracji, co kod produkcyjny. To znaczy. do niczego.
Martin Mucha
Przepraszam. Będę biczować mnie z powodu tego błędu. Spróbuj jeszcze raz!
JRichardsz
Działa to również w 2.0.0.RELEASEprzypadku Spring Boot.
Nital
Powinieneś użyć wbudowanej
bazy danych do
7

Jeśli naprawdę chcesz napisać i-test dla wiosennego repozytorium danych, możesz to zrobić w następujący sposób:

@RunWith(SpringRunner.class)
@DataJpaTest
@EnableJpaRepositories(basePackageClasses = WebBookingRepository.class)
@EntityScan(basePackageClasses = WebBooking.class)
public class WebBookingRepositoryIntegrationTest {

    @Autowired
    private WebBookingRepository repository;

    @Test
    public void testSaveAndFindAll() {
        WebBooking webBooking = new WebBooking();
        webBooking.setUuid("some uuid");
        webBooking.setItems(Arrays.asList(new WebBookingItem()));
        repository.save(webBooking);

        Iterable<WebBooking> findAll = repository.findAll();

        assertThat(findAll).hasSize(1);
        webBooking.setId(1L);
        assertThat(findAll).containsOnly(webBooking);
    }
}

Aby podążać za tym przykładem, musisz użyć następujących zależności:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
Philipp Wirth
źródło
5

Rozwiązałem to, używając w ten sposób -

    @RunWith(SpringRunner.class)
    @EnableJpaRepositories(basePackages={"com.path.repositories"})
    @EntityScan(basePackages={"com.model"})
    @TestPropertySource("classpath:application.properties")
    @ContextConfiguration(classes = {ApiTestConfig.class,SaveActionsServiceImpl.class})
    public class SaveCriticalProcedureTest {

        @Autowired
        private SaveActionsService saveActionsService;
        .......
        .......
}
Ajay Kumar
źródło
4

Z JUnit5 i @DataJpaTesttest będzie wyglądał następująco (kod kotlin):

@DataJpaTest
@ExtendWith(value = [SpringExtension::class])
class ActivityJpaTest {

    @Autowired
    lateinit var entityManager: TestEntityManager

    @Autowired
    lateinit var myEntityRepository: MyEntityRepository

    @Test
    fun shouldSaveEntity() {
        // when
        val savedEntity = myEntityRepository.save(MyEntity(1, "test")

        // then 
        Assertions.assertNotNull(entityManager.find(MyEntity::class.java, savedEntity.id))
    }
}

Możesz użyć TestEntityManagerz org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpakietu, aby sprawdzić stan jednostki.

Przemek Nowak
źródło
Zawsze lepiej jest wygenerować identyfikator dla fasoli encji.
Arundev,
Dla Javy druga linia to: @ExtendWith (value = SpringExtension.class)
AdilOoze