web-dev-qa-db-ja.com

Spring DataリポジトリでSpringの宣言的キャッシングサポートをテストする方法は?

org.springframework.data.jpa.repository.JpaRepositoryを拡張するSpring DataリポジトリMemberRepositoryインターフェイスを開発しました。 MemberRepositoryにはメソッドがあります:

@Cacheable(CacheConfiguration.DATABASE_CACHE_NAME)
Member findByEmail(String email);

結果は、Springキャッシュ抽象化によってキャッシュされます(ConcurrentMapCacheによって裏付けられます)。

私が持っている問題は、結果が最初にdbから取得されたことをアサートする(hsqldbに対する)統合テストを書きたいことです2回目のキャッシュから

私は当初、JPAインフラストラクチャ(エンティティマネージャなど)をモックすることを考え、何らかの理由でエンティティマネージャが2度目に呼び出されないことを主張しますが、あまりにも難しい/扱いにくいようです( https://stackoverflow.com/aを参照)/23442457/536299 )。

誰かが@Cacheableで注釈されたSpring Data Repositoryメソッドのキャッシュ動作をテストする方法についてアドバイスを提供できますか?

28
balteo

キャッシングなどの技術的な側面をテストする場合は、データベースをまったく使用しないでください。ここでテストする内容を理解することが重要です。確実にしたいのですが、メソッドの呼び出しは、まったく同じ引数を使用した呼び出しでは回避されます。データベースの前にあるリポジトリは、このトピックと完全に直交する側面です。

私がお勧めするものは次のとおりです。

  1. 宣言的キャッシングを構成する統合テストをセットアップします(または、実動構成から必要なビットとピースをインポートします)。
  2. リポジトリの模擬インスタンスを構成します。
  3. テストケースを作成して、モックの予想される動作を設定し、メソッドを呼び出して、出力を適切に検証します。

サンプル

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class CachingIntegrationTest {

  // Your repository interface
  interface MyRepo extends Repository<Object, Long> {

    @Cacheable("sample")
    Object findByEmail(String email);
  }

  @Configuration
  @EnableCaching
  static class Config {

    // Simulating your caching configuration
    @Bean
    CacheManager cacheManager() {
      return new ConcurrentMapCacheManager("sample");
    }

    // A repository mock instead of the real proxy
    @Bean
    MyRepo myRepo() {
      return Mockito.mock(MyRepo.class);
    }
  }

  @Autowired CacheManager manager;
  @Autowired MyRepo repo;

  @Test
  public void methodInvocationShouldBeCached() {

    Object first = new Object();
    Object second = new Object();

    // Set up the mock to return *different* objects for the first and second call
    Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second);

    // First invocation returns object returned by the method
    Object result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Second invocation should return cached value, *not* second (as set up above)
    result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Verify repository method was invoked once
    Mockito.verify(repo, Mockito.times(1)).findByEmail("foo");
    assertThat(manager.getCache("sample").get("foo"), is(notNullValue()));

    // Third invocation with different key is triggers the second invocation of the repo method
    result = repo.findByEmail("bar");
    assertThat(result, is(second));
  }
}

ご覧のとおり、ここでは少しオーバーテストを行っています。

  1. 最も関連性の高いチェックは、2番目の呼び出しが最初のオブジェクトを返すことだと思います。それがキャッシングのすべてです。同じキーを持つ最初の2つの呼び出しは同じオブジェクトを返しますが、異なるキーを持つ3番目の呼び出しは、リポジトリでの2番目の実際の呼び出しになります。
  2. キャッシュに実際に最初のキーの値があることを確認することにより、テストケースを強化します。実際の値を確認するために拡張することもできます。一方、アプリケーションレベルの動作ではなく、メカニズムの内部をより多くテストする傾向があるので、それを避けるのも良いと思います。

重要なポイント

  1. コンテナの動作をテストするためにインフラストラクチャを配置する必要はありません。
  2. テストケースの設定は簡単で簡単です。
  3. 適切に設計されたコンポーネントを使用すると、簡単なテストケースを記述でき、テストのための統合作業が少なくて済みます。
58
Oliver Drotbohm

Oliverの例を使用して、アプリのキャッシュ動作をテストしてみました。私の場合、キャッシュはサービス層に設定されており、リポジトリが適切な回数呼び出されていることを確認したいと思います。私はmockitoの代わりにspock mocksを使用しています。最初に実行されているテストがキャッシュに格納され、他のテストに影響を与えることに気付くまで、テストが失敗する理由を理解しようとして少し時間を費やしました。すべてのテストでキャッシュをクリアした後、期待どおりに動作し始めました。

ここに私が終わったものがあります:

@ContextConfiguration
class FooBarServiceCacheTest extends Specification {

  @TestConfiguration
  @EnableCaching
  static class Config {

    def mockFactory = new DetachedMockFactory()
    def fooBarRepository = mockFactory.Mock(FooBarRepository)

    @Bean
    CacheManager cacheManager() {
      new ConcurrentMapCacheManager(FOOBARS)
    }

    @Bean
    FooBarRepository fooBarRepository() {
      fooBarRepository
    }

    @Bean
    FooBarService getFooBarService() {
      new FooBarService(fooBarRepository)
    }
  }

  @Autowired
  @Subject
  FooBarService fooBarService

  @Autowired
  FooBarRepository fooBarRepository

  @Autowired
  CacheManager cacheManager

  def "setup"(){
    // we want to start each test with an new cache
    cacheManager.getCache(FOOBARS).clear()
  }

  def "should return cached foobars "() {

    given:
    final foobars = [new FooBar(), new FooBar()]

    when:
    fooBarService.getFooBars()
    fooBarService.getFooBars()
    final fooBars = fooBarService.getFooBars()

    then:
    1 * fooBarRepository.findAll() >> foobars
  }

def "should return new foobars after clearing cache"() {

    given:
    final foobars = [new FooBar(), new FooBar()]

    when:
    fooBarService.getFooBars()
    fooBarService.clearCache()
    final fooBars = fooBarService.getFooBars()

    then:
    2 * fooBarRepository.findAll() >> foobars
  }
} 
1
Mustafa