1. 概要

Apache Cassandra は、オープンソースの分散型NoSQLデータベースです。 これは、大量のデータを高速の読み取り/書き込みパフォーマンスで処理し、単一障害点がないように設計されています。

このチュートリアルでは、Cassandraデータベースを使用するSpring Bootアプリケーションのテストについて説明します。 TestcontainersライブラリのCassandraコンテナを使用して統合テストを設定する方法を説明します。 さらに、Spring Dataリポジトリの抽象化を利用して、Cassandraのデータレイヤーを操作します。

最後に、複数の統合テストで共有Cassandraコンテナインスタンスを再利用する方法を示します。

2. テストコンテナ

Testcontainersは、Dockerコンテナの軽量で使い捨てのインスタンスを提供するJavaライブラリです。 したがって、データベースを使用するアプリケーションの統合テストのために、Springで一般的に使用します。 Testcontainersを使用すると、ローカルマシンにデータベースをインストールして管理しなくても、実際のデータベースインスタンスでテストできます。

2.1. Mavenの依存関係

Cassandraコンテナは、CassandraTestcontainersモジュールで利用できます。 これにより、コンテナ化されたCassandraインスタンスを使用できるようになります。

cassandra-unit ライブラリとは異なり、TestcontainersライブラリはJUnit5と完全に互換性があります。 必要なMaven依存関係をリストすることから始めましょう:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>cassandra</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
<dependency>

2.2. カサンドラコンテナ

コンテナ化されたデータベースインスタンスは、統合テストに一般的に使用されます。 また、は、データアクセス層コードが特定のデータベースバージョンと完全に互換性があることを確認します

まず、テストクラスに@SpringBootTest@Testcontainersの両方でアノテーションを付ける必要があります。

@SpringBootTest
@Testcontainers
class CassandraSimpleIntegrationTest {}

次に、Cassandraコンテナを定義し、その固有のポートを公開できます。

@Container
public static final CassandraContainer cassandra 
  = (CassandraContainer) new CassandraContainer("cassandra:3.11.2").withExposedPorts(9042);

ここでは、コンテナポート 9042を公開しています。ただし、Testcontainersは、後で取得できるランダムなホストポートにリンクすることに注意してください。

上記を使用して、Testcontainersライブラリは、テストクラスのライフサイクルに合わせてドッキングされたCassandraコンテナインスタンスの開始を自動的に処理します。

@Test
void givenCassandraContainer_whenSpringContextIsBootstrapped_thenContainerIsRunningWithNoExceptions() {
    assertThat(cassandra.isRunning()).isTrue();
}

これで、実行中のCassandraコンテナができました。 ただし、Springアプリケーションはまだそれを認識していません。

2.3. テストプロパティのオーバーライド

Spring DataがCassandraコンテナとの接続を確立できるようにするには、いくつかの接続プロパティを提供する必要があります。 java .lang.System クラスを介してシステムプロパティを定義することにより、デフォルトのCassandra接続プロパティをオーバーライドします。

@BeforeAll
static void setupCassandraConnectionProperties() {
    System.setProperty("spring.data.cassandra.keyspace-name", KEYSPACE_NAME);
    System.setProperty("spring.data.cassandra.contact-points", cassandra.getContainerIpAddress());
    System.setProperty("spring.data.cassandra.port", String.valueOf(cassandra.getMappedPort(9042)));
}

これで、Cassandraコンテナに接続するようにSpringDataを構成しました。 ただし、キースペースを作成する必要があります。

2.4. キースペースの作成

Cassandra でテーブルを作成する前の最後のステップとして、キースペースを作成する必要があります。

private static void createKeyspace(Cluster cluster) {
    try (Session session = cluster.connect()) {
        session.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE_NAME +
          " WITH replication = \n" +
          "{'class':'SimpleStrategy','replication_factor':'1'};");
    }
}

Cassandraのキースペースは、RDBMSのデータベースと非常によく似ています。 これは、Cassandraクラスター内のノードでデータがどのように複製されるかを定義します。

3. カサンドラの春のデータ

ApacheCassandraのSpringデータはCassandraを使用するアプリケーションの開発にコアSpringの概念を適用します。 リポジトリ、クエリビルダー、およびリッチオブジェクトマッピング用の単純な注釈を提供します。 したがって、さまざまなデータベースを使用するSpring開発者に使い慣れたインターフェイスを提供します。

3.1. データアクセスオブジェクト

統合テストの後半で使用する簡単なDAOクラスを準備することから始めましょう。

@Table
public class Car {

    @PrimaryKey
    private UUID id;
    private String make;
    private String model;
    private int year;

    public Car(UUID id, String make, String model, int year) {
        this.id = id;
        this.make = make;
        this.model = model;
        this.year = year;
    }

    //getters, setters, equals and hashcode
}

ここで重要なのは、org.springframework.data.cassandra.core.mappingパッケージの@Tableアノテーションでクラスにアノテーションを付けることです。 実際、このアノテーションにより、ドメインオブジェクトの自動マッピングが可能になります。

3.2. Cassandraリポジトリ

Spring Dataを使用すると、DAOのリポジトリを非常に簡単に作成できます。 まず、Spring BootメインクラスでCassandraリポジトリを有効にする必要があります。

@SpringBootApplication
@EnableCassandraRepositories(basePackages = "org.baeldung.springcassandra.repository")
public class SpringCassandraApplication {}

次に、CassandraRepositoryを拡張するインターフェイスを作成する必要があります。

@Repository
public interface CarRepository extends CassandraRepository<Car, UUID> {}

統合テストを開始する前に、次の2つの追加プロパティを定義する必要があります。

spring.data.cassandra.local-datacenter=datacenter1
spring.data.cassandra.schema-action=create_if_not_exists

最初のプロパティは、デフォルトのローカルデータセンター名を定義します。 2つ目は、SpringDataが必要なデータベーステーブルを自動的に作成することを保証します。 この設定は本番システムでは使用しないでください。

Testcontainersを使用しているので、テストが終了したらテーブルを削除することを心配する必要はありません。 テストを実行するたびに、新しいコンテナが開始されます。

4. 統合テスト

これで、Cassandraコンテナー、単純なDAOクラス、およびSpring Dataリポジトリーがセットアップされたので、統合テストの作成を開始する準備が整いました。

4.1. 記録テストの保存

Cassandraデータベースへの新しいレコードの挿入をテストすることから始めましょう。

@Test
void givenValidCarRecord_whenSavingIt_thenRecordIsSaved() {
    UUID carId = UUIDs.timeBased();
    Car newCar = new Car(carId, "Nissan", "Qashqai", 2018);

    carRepository.save(newCar);

    List<Car> savedCars = carRepository.findAllById(List.of(carId));
    assertThat(savedCars.get(0)).isEqualTo(newCar);
}

4.2. レコードテストの更新

次に、既存のデータベースレコードを更新するための同様のテストを作成できます。

@Test
void givenExistingCarRecord_whenUpdatingIt_thenRecordIsUpdated() {
    UUID carId = UUIDs.timeBased();
    Car existingCar = carRepository.save(new Car(carId, "Nissan", "Qashqai", 2018));

    existingCar.setModel("X-Trail");
    carRepository.save(existingCar);

    List<Car> savedCars = carRepository.findAllById(List.of(carId));
    assertThat(savedCars.get(0).getModel()).isEqualTo("X-Trail");
}

4.3. レコードテストの削除

最後に、既存のデータベースレコードを削除するためのテストを作成しましょう。

@Test
void givenExistingCarRecord_whenDeletingIt_thenRecordIsDeleted() {
    UUID carId = UUIDs.timeBased();
    Car existingCar = carRepository.save(new Car(carId, "Nissan", "Qashqai", 2018));

    carRepository.delete(existingCar);

    List<Car> savedCars = carRepository.findAllById(List.of(carId));
    assertThat(savedCars.isEmpty()).isTrue();
}

5. 共有コンテナインスタンス

ほとんどの場合、統合テストを使用する場合は、複数のテストで同じデータベースインスタンスを再利用します。 複数のネストされたテストクラスを利用することで、同じコンテナインスタンスを共有できます。

@Testcontainers
@SpringBootTest
class CassandraNestedIntegrationTest {

    private static final String KEYSPACE_NAME = "test";

    @Container
    private static final CassandraContainer cassandra 
      = (CassandraContainer) new CassandraContainer("cassandra:3.11.2").withExposedPorts(9042);

    // Set connection properties and create keyspace

    @Nested
    class ApplicationContextIntegrationTest {
        @Test
        void givenCassandraContainer_whenSpringContextIsBootstrapped_thenContainerIsRunningWithNoExceptions() {
            assertThat(cassandra.isRunning()).isTrue();
        }
    }

    @Nested
    class CarRepositoryIntegrationTest {

        @Autowired
        private CarRepository carRepository;

        @Test
        void givenValidCarRecord_whenSavingIt_thenRecordIsSaved() {
            UUID carId = UUIDs.timeBased();
            Car newCar = new Car(carId, "Nissan", "Qashqai", 2018);

            carRepository.save(newCar);

            List<Car> savedCars = carRepository.findAllById(List.of(carId));
            assertThat(savedCars.get(0)).isEqualTo(newCar);
        }

        // Tests for update and delete
    }
}

Dockerコンテナーは起動に時間がかかるため、複数のネストされたテストクラス間で共有コンテナーインスタンスを使用すると、実行が高速になります。 ただし、この共有インスタンスはテスト間で自動的にクリアされないことに注意してください。

6. 結論

この記事では、Cassandraデータベースを使用するSpringBootアプリケーションをテストするためにCassandraコンテナーを使用するについて説明しました。

例では、ドッキングされたCassandraコンテナインスタンスの設定、テストプロパティのオーバーライド、キースペース、DAOクラス、およびCassandraリポジトリインターフェイスの作成について説明しました。

Cassandraコンテナを利用する統合テストの書き方を見ました。 したがって、サンプルテストではモックは必要ありませんでした。 最後に、ネストされた複数のテストクラス間で同じコンテナインスタンスを再利用する方法を確認しました。

いつものように、ソースコードはGitHubから入手できます。