Как заменить @Sql в тестах на Spring data r2dbc?

#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>
  

Примечание:

  1. область видимости для зависимости 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();
    }
}

  

Примечание:

  1. Обратитесь к следующему сокращенному коду для подключения к 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() {}
}
  

Примечание:

  1. 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 .