1. ホーム
  2. unit-testing

[解決済み] Spring Dataのリポジトリをテストするには?

2022-04-22 03:44:49

質問

リポジトリが欲しい(例えば UserRepository )をSpring Dataの助けを借りて作成しました。私はspring-dataに新しいです(しかし、春にはない)、私はこれを使用します。 チュートリアル . データベースを扱うために私が選択した技術は、JPA 2.1とHibernateです。問題は、このようなリポジトリに対するユニットテストをどのように書くかについて、私は無知であるということです。

それでは create() メソッドを例にとって説明します。私はテストファーストで作業しているので、このメソッドのユニットテストを書くことになっているのですが、ここで3つの問題にぶつかりました。

  • まず、このモックに EntityManager の存在しない実装に UserRepository インターフェイスですか?Spring Dataはこのインターフェイスに基づいた実装を生成することになります。

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

    しかし、どうすれば強制的に EntityManager モックなどです。もし私が自分で実装を書いたとしたら、おそらくセッター・メソッドで EntityManager このモックを使って、ユニットテストを行うことができます。(実際のデータベース接続に関しては、私は JpaConfiguration クラスで、アノテーションは @Configuration@EnableJpaRepositories のビーンをプログラム的に定義するものです。 DataSource , EntityManagerFactory , EntityManager などがあります。- しかし、リポジトリはテストフレンドリーであるべきで、これらのことをオーバーライドできるようにすべきです)。

  • 次に、インタラクションのテストはした方がいいのでしょうか?のどのメソッドを使えばいいのか、私にはわかりません。 EntityManagerQuery が呼び出されることになっています(あの verify(entityManager).createNamedQuery(anyString()).getResultList(); )、実装を書いているのは私ではないので。

  • 第三に、そもそもSpring-Dataが生成したメソッドをユニットテストすることになっているのでしょうか?私の知る限り、サードパーティライブラリのコードはユニットテストされないことになっています - 開発者が自分で書いたコードだけがユニットテストされることになっています。しかし、もしそれが本当なら、やはり最初の疑問が浮かび上がってきます:例えば、私のリポジトリにいくつかのカスタムメソッドがあり、それの実装を書くことになった場合、どのようにして EntityManagerQuery を最終的に生成されたリポジトリに入れるのですか?

注意:私は自分のリポジトリをテストドライブするために どちらも 統合テストと単体テストです。統合テストではHSQLのインメモリデータベースを使用し、ユニットテストでは明らかにデータベースを使用しません。

そして、おそらく4番目の質問ですが、統合テストで正しいオブジェクトグラフの作成とオブジェクトグラフの検索をテストすることは正しいですか(例えば、Hibernateで複雑なオブジェクトグラフを定義している場合)?

更新:今日はモックインジェクションの実験を続けました。モックインジェクションを可能にするために静的なインナークラスを作りました。

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

}

しかし、このテストを実行すると、次のようなスタックトレースが表示されます。

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

解決方法は?

tl;dr

簡単に説明すると、Spring Data JPAリポジトリを合理的にユニットテストする方法はありません。ユニットテストはあまり意味がありません。実装コードを自分で書くことは通常ないので(カスタム実装に関する以下の段落を参照)、統合テストが最も合理的なアプローチとなります。

詳細

無効な派生クエリなどがないアプリのみをブートストラップできるように、かなり多くの事前検証やセットアップを行っています。

  • を作成し、キャッシュします。 CriteriaQuery インスタンスを作成し、クエリメソッドにタイポが含まれていないことを確認します。これは meta.model と同様に Criteria API で作業する必要があります。
  • 手動で定義したクエリを検証するために EntityManager を作成し Query のインスタンスを作成します (これは効果的にクエリ構文検証のトリガーとなります)。
  • を検査します。 Metamodel のメタデータを取得し、is-newチェックなどを準備する。

手書きのリポジトリでは、(無効なクエリなどにより)実行時にアプリケーションが壊れる可能性があるため、おそらく延期するようなものばかりです。

考えてみれば、リポジトリのために書くコードはないので ユニット をテストします。基本的なバグは私たちのテストベースに任せておけばよいので、その必要はありません。 チケット ). しかし、永続化レイヤーの2つの側面をテストするための統合テストは、あなたのドメインに関連する側面であるため、間違いなく必要です。

  • エンティティマッピング
  • クエリのセマンティクス(構文はブートストラップごとに検証されます)。

統合テスト

これは通常、インメモリデータベースと、Spring APIをブートストラップするテストケースを使用して行われます。 ApplicationContext 通常、テストコンテキストフレームワークを通じて (すでに行っているように) データベースに事前にデータを入れ、 (オブジェクトのインスタンスを EntityManager やレポ、あるいはプレーンなSQLファイルを介して)クエリメソッドを実行し、その結果を検証します。

カスタム実装のテスト

レポジトリのカスタム実装部分は 書き方 というのは、Spring Data JPAを知らなくてもいいようにするためです。それらは単なるSpringビーンであり EntityManager を注入しています。もちろん、モックを使ってインタラクションを試してみたいかもしれませんが、正直なところ、JPAのユニットテストは私たちにとってあまり楽しい経験ではありませんでした。 EntityManager -> CriteriaBuilder , CriteriaQuery など)を使って、モックがモックを返すというようなことが起こってしまいます。