Перейти к основному содержимому

Использование тестовых контейнеров с Spring Data Cassandra

· 6 мин. чтения

1. Обзор

Apache Cassandra — это распределенная база данных NoSQL с открытым исходным кодом. Он был разработан для обработки больших объемов данных с высокой производительностью чтения-записи и без единой точки отказа .

В этом руководстве мы рассмотрим тестирование приложения Spring Boot, использующего базу данных Cassandra. Мы объясним, как настроить интеграционные тесты с помощью контейнера Cassandra из библиотеки Testcontainers . Кроме того, мы будем использовать абстракцию репозитория Spring Data для работы со слоем данных Cassandra.

Наконец, мы покажем, как повторно использовать общий экземпляр контейнера Cassandra в нескольких интеграционных тестах.

2. Тестовые контейнеры

Testcontainers — это библиотека Java, предоставляющая легкие одноразовые экземпляры контейнеров Docker . Следовательно, мы обычно используем его в Spring для интеграционного тестирования приложений, использующих базы данных. Testcontainers позволяет нам тестировать реальный экземпляр базы данных, не требуя от нас установки и управления базой данных на нашем локальном компьютере.

2.1. Зависимости Maven

Контейнеры Cassandra доступны в модуле Cassandra Testcontainers . Это позволяет использовать контейнерные экземпляры Cassandra.

В отличие от библиотеки cassandra -unit , библиотека Testcontainers полностью совместима с JUnit 5 . Начнем с перечисления необходимых зависимостей 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 , нам нужно указать несколько свойств соединения. Мы переопределим свойства подключения Cassandra по умолчанию, определив системные свойства через класс java.lang.System :

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

Теперь мы настроили Spring Data для подключения к нашему контейнеру Cassandra. Однако нам все равно нужно создать пространство ключей.

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 очень похоже на базу данных в СУБД. Он определяет, как данные реплицируются на узлах в кластере Cassandra.

3. Весенние данные для Кассандры

Spring Data для Apache Cassandra применяет основные концепции Spring к разработке приложений с использованием Cassandra . Он предоставляет репозитории, построители запросов и простые аннотации для расширенного сопоставления объектов. Таким образом, он предлагает знакомый интерфейс для разработчиков 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
}

Ключевым моментом здесь является аннотирование класса аннотацией @Table из пакета org.springframework.data.cassandra.core.mapping . Фактически, эта аннотация позволяет автоматически отображать объекты домена.

3.2. Репозиторий Кассандры

Spring Data позволяет очень просто создать репозиторий для нашего DAO. Для начала нам нужно включить репозитории Cassandra в нашем основном классе Spring Boot:

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

Затем нам просто нужно создать интерфейс, расширяющий CassandraRepository :

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

Прежде чем приступить к интеграционным тестам, нам нужно определить два дополнительных свойства:

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

Первое свойство определяет имя локального центра обработки данных по умолчанию. Второй гарантирует, что Spring Data автоматически создаст для нас необходимые таблицы базы данных. Следует отметить, что этот параметр не следует использовать в производственных системах .

Поскольку мы используем 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 для тестирования приложения Spring Boot , использующего базу данных Cassandra.

В примерах мы рассмотрели настройку докеризованного экземпляра контейнера Cassandra, переопределение тестовых свойств, создание пространства ключей, класса DAO и интерфейса репозитория Cassandra.

Мы увидели, как писать интеграционные тесты, использующие контейнер Cassandra. Таким образом, наши примеры тестов не требовали насмешек. Наконец, мы увидели, как повторно использовать один и тот же экземпляр контейнера в нескольких вложенных классах тестов.

Как всегда, исходный код доступен на GitHub .