#spring-data #project-reactor #spring-data-r2dbc
#spring-данные #проект-реактор #spring-data-r2dbc
Вопрос:
В Spring data JPA есть @Sql
аннотация, которая очень удобна для настройки интеграционных тестов для уровня сохраняемости. Он может развертывать тестовые данные перед каждым тестом и выполнять очистку после него.
Однако я не смог найти его в spring-data-r2dbc
модуле. Есть ли что-нибудь подобное, чтобы легко справиться с этой задачей spring-data-r2dbc
?
Ответ №1:
На данный момент я не нашел ничего лучше, чем использовать org.springframework.data.r2dbc.connectionfactory.init.ScriptUtils#executeSqlScript(io.r2dbc.spi.Connection, org.springframework.core.io.Resource)
вместе с JUnit @BeforeEach
и @AfterEach
тестовыми обратными вызовами:
@Autowired
private ConnectionFactory connectionFactory;
private void executeScriptBlocking(final Resource sqlScript) {
Mono.from(connectionFactory.create())
.flatMap(connection -> ScriptUtils.executeSqlScript(connection, sqlScript))
.block();
@BeforeEach
private void rollOutTestData(@Value("classpath:/db/insert_test_data.sql") Resource script) {
executeScriptBlocking(script);
}
@AfterEach
private void cleanUpTestData(@Value("classpath:/db/delete_test_data.sql") Resource script) {
executeScriptBlocking(script);
}
Примечание: здесь я использую JUnit5 с jupiter API
Комментарии:
1. спасибо, что сэкономило мне несколько часов исследований и работает нормально.
Ответ №2:
Это еще одна альтернатива, которую вы можете попробовать:
@BeforeEach
fun populateTestData(@Value("classpath:test-data.sql") testDataSql: Resource, @Autowired connectionFactory: ConnectionFactory) {
val resourceDatabasePopulator = ResourceDatabasePopulator()
resourceDatabasePopulator.addScript(testDataSql)
resourceDatabasePopulator.populate(connectionFactory).block()
}
Ответ №3:
Рассмотрим следующий шаг для использования @Sql
:
Шаг 1: добавление зависимости
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
Примечание:
- область видимости для зависимости
spring-boot-starter-jdbc
таковаtest
, что нам нужен только @Sql во время тестирования JUnit.
Шаг 2: Реализация org.springframework.test.context.TestExecutionListener
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import org.springframework.test.context.jdbc.SqlGroup;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;
public class SqlScriptsR2dbcTestExecutionListener implements TestExecutionListener {
private static final Logger LOGGER = LoggerFactory.getLogger(SqlScriptsR2dbcTestExecutionListener.class);
public void beforeTestClass(TestContext testContext) throws Exception {
}
public void beforeTestMethod(TestContext testContext) throws Exception {
executeSqlScripts(testContext, BEFORE_TEST_METHOD);
}
public void afterTestMethod(TestContext testContext) throws Exception {
executeSqlScripts(testContext, AFTER_TEST_METHOD);
}
/**
* Execute SQL scripts configured via {@link Sql @Sql} for the supplied
* {@link TestContext} and {@link ExecutionPhase}.
*/
private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) throws Exception {
boolean classLevel = false;
Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(testContext.getTestMethod(),
Sql.class, SqlGroup.class);
if (sqlAnnotations.isEmpty()) {
sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(testContext.getTestClass(), Sql.class,
SqlGroup.class);
if (!sqlAnnotations.isEmpty()) {
classLevel = true;
}
}
for (Sql sql : sqlAnnotations) {
executeSqlScripts(sql, executionPhase, testContext, classLevel);
}
}
private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestContext testContext,
boolean classLevel) throws IOException {
if (sql == null || sql.scripts() == null || sql.scripts().length < 1) {
return ;
}
ApplicationContext applicationContext = testContext.getApplicationContext();
ConnectionFactory connectionFactory = applicationContext.getBean(ConnectionFactory.class);
List<FileSystemResource> resources = Arrays.asList(sql.scripts())
.stream()
.peek(path -> LOGGER.info("Script path: {}", path))
.map(scriptPath -> new FileSystemResource(scriptPath))
.toList();
Resource []scriptResources = resources.toArray(new Resource[resources.size()]);
create(connectionFactory, scriptResources);
}
private void create(ConnectionFactory connectionFactory, Resource [] resources) throws IOException {
final StringBuilder sb = new StringBuilder();
for (Resource resource: resources) {
sb.append(Files.readString(Paths.get(resource.getURI()), Charset.forName("UTF-8")));
}
Mono.from(connectionFactory.create())
.flatMapMany(connection -> connection.createStatement(sb.toString())
.execute())
.subscribe();
}
}
Примечание:
- Обратитесь к следующему сокращенному коду для подключения к JDBC в приведенном выше сокращенном коде:
ConnectionFactory connectionFactory = ConnectionFactories
.get("r2dbc:h2:mem:///testdb");
Mono.from(connectionFactory.create())
.flatMapMany(connection -> connection
.createStatement("SELECT firstname FROM PERSON WHERE age > $1")
.bind("$1", 42)
.execute())
.flatMap(result -> result
.map((row, rowMetadata) -> row.get("firstname", String.class)))
.doOnNext(System.out::println)
.subscribe();
Шаг 3: реализация в тестовом примере JUnit 5
@DataR2dbcTest
@ActiveProfiles(value = "test")
@TestExecutionListeners(value = {
DependencyInjectionTestExecutionListener.class,
SqlScriptsR2dbcTestExecutionListener.class
})
class SampleRepositoryTest {
@Tags(value = {
@Tag(value = "r2dbc"),
@Tag(value = "save"),
@Tag(value = "findAll")
})
@Sql(scripts = {
SCHEMA_H2_SQL
}, executionPhase = BEFORE_TEST_METHOD)
@Test
void test() {}
}
Примечание:
DependencyInjectionTestExecutionListener.class
требуется, чтобы убедиться, что @Autowired не должен быть нулевым во время выполнения тестового примера.
Шаг 4: Настройка в application-test.properties
spring.r2dbc.url=r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.r2dbc.username=sa
spring.r2dbc.password=sa
spring.r2dbc.initialization-mode=always
Шаг 5: запрос постоянного файла sql scripts
public class JpaConstant {
private JpaConstant() {}
public static final String CONFIG_FOLDER = "src/main/resources/config/";
public static final String SCHEMA_H2_SQL = CONFIG_FOLDER "schema-h2.sql";
}
Ответ №4:
Если вы хотите, вы можете использовать @Sql
в своих интеграционных тестах простым способом, чтобы вы могли использовать то, с чем вы знакомы. Даже если вы используете r2dbc, вы можете добавить эти зависимости в свой файл pom.xml / gradle:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
итак, у вас есть оба драйвера (блокирующая и неблокирующая версии). Эта зависимость может быть полезна в другой ситуации (например, если вы используете Flyway — он пока не поддерживает реактивные драйверы). Затем вы можете создать @Configuration
класс, который вы можете использовать в своих интеграционных тестах. В случае, если вы используете postgresql в своем интеграционном тесте, у вас будет что-то вроде:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class TestConfig {
/**
* Necessary to run sql scripts with @Sql
*/
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost:5432/postgres");
dataSource.setUsername("postgres");
dataSource.setPassword("1234");
return dataSource;
}
}
Чтобы протестировать вашу базу данных, ваш тест начнется с чего-то вроде:
@DataR2dbcTest
@ActiveProfiles("test")
@Sql(value = "classpath:sql/MovieInfoRepositoryITest.sql")
@Import({TestConfig.class})
class MovieInfoRepositoryITest {
...
}
Не бойтесь использовать диск JDBC там, где это не «болезненно» для вашего реактивного приложения, например, выполнение ваших sql-скриптов во время тестов.
Комментарии:
1. Это не сработает. Аннотация
@Sql
откроет транзакцию перед вызовом метода тестирования и запросом@Sql
в не зафиксированном, поэтому, если реактивный верификатор время от времени выполняется в другом потоке (планировщике), реактивный код не будет видеть данные@Sql
.