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
EntityManager
do nieistniejącej implementacjiUserRepository
interfejsu? 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
EntityManager
mocka i innych mocków - gdybym sam napisał implementację, prawdopodobnie miałbym metodę ustawiającą dlaEntityManager
, która pozwoliłaby mi użyć mojego mocka do testu jednostkowego. (Jeśli chodzi o rzeczywistą łączność z bazami danych, mamJpaConfiguration
klasę, z uwagami@Configuration
i@EnableJpaRepositories
, który programowo określa fasolaDataSource
,EntityManagerFactory
,EntityManager
itd. - 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
EntityManager
i jakieQuery
mają być wywoływane (podobnieverify(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
EntityManager
iQuery
do 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
źródło
pom.xml
.Dzięki Spring Boot + Spring Data stało się to całkiem proste:
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.
źródło
@RunWith(SpringRuner.class)
jest już uwzględniony w@DataJpaTest
.@RunWith(SpringRunner.class
uruchamia kontekst sprężynowy, co oznacza, że sprawdza integrację między kilkoma jednostkami. Test jednostkowy to testowanie pojedynczej jednostki -> pojedynczej klasy. Następnie piszeszMyClass sut = new MyClass();
i testujesz obiekt sut (sut = usługa w trakcie testu)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.
źródło
Jeśli używasz Spring Boota, możesz po prostu użyć go
@SpringBootTest
do załadowania swojegoApplicationContext
(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:Możesz przeczytać więcej o testowaniu w wiosennym rozruchu w ich dokumentacji .
źródło
Predicate
s (co było moim przypadkiem użycia), działa całkiem dobrze.W ostatniej wersji Spring Boot 2.1.1.RELEASE jest to proste:
Kompletny kod:
https://github.com/jrichardsz/spring-boot-templates/blob/master/003-hql-database-with-integration-test/src/test/java/test/CustomerRepositoryIntegrationTest.java
źródło
2.0.0.RELEASE
przypadku Spring Boot.Jeśli naprawdę chcesz napisać i-test dla wiosennego repozytorium danych, możesz to zrobić w następujący sposób:
Aby podążać za tym przykładem, musisz użyć następujących zależności:
źródło
Rozwiązałem to, używając w ten sposób -
źródło
Z JUnit5 i
@DataJpaTest
test będzie wyglądał następująco (kod kotlin):Możesz użyć
TestEntityManager
zorg.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
pakietu, aby sprawdzić stan jednostki.źródło