【问题标题】:How to mock an ItemReader in a Spring Batch application using Spock and Groovy如何使用 Spock 和 Groovy 在 Spring Batch 应用程序中模拟 ItemReader
【发布时间】:2020-06-05 09:31:09
【问题描述】:

我正在尝试为 Spring Batch 应用程序编写 测试,特别是在获取记录时来自以下 reader 的交互来自实现简单 RowMapper 的数据库:

@Component
@StepScope
public class RecordItemReader extends JdbcCursorItemReader<FooDto> {
  @Autowired
  public RecordItemReader(DataSource dataSource) {
    this.setDataSource(dataSource);
    this.setSql(AN_SQL_QUERY);
    this.setRowMapper(new RecordItemMapper());
  }
}

这是批处理配置中的步骤定义:

  @Bean
  public Step step(RecordItemReader recordItemReader,
                   BatchSkipListener skipListener,
                   RecordItemWriter writer,
                   RecordItemProcessor processor,
                   PlatformTransactionManager transactionManager) {
    return stepBuilderFactory
      .get("step1")
      .transactionManager(transactionManager)
      .reader(recordItemReader)
      .faultTolerant()
      .skip(ParseException.class)
      .skip(UnexpectedInputException.class)
      .skipPolicy(new AlwaysSkipItemSkipPolicy())
      .listener(skipListener)
      .processor(processor)
      .writer(writer)
      .build();
  }

一切正常,除非我尝试使用以下内容进行测试:

@SpringBatchTest
@EnableAutoConfiguration
class BatchSOTest extends Specification {

  @Resource
  JobLauncherTestUtils jobLauncherTestUtils

  @Resource
  JobRepositoryTestUtils jobRepositoryTestUtils

  @Resource
  RecordItemReader recordItemReader

  def cleanup() {
    jobRepositoryTestUtils.removeJobExecutions()
  }

  def "batch init perfectly"() {
    given:
    // this does not work :
    (1.._) * recordItemReader.read() >> null

    when:
    def jobExecution = jobLauncherTestUtils.launchJob()
    def jobInstance = jobExecution.getJobInstance()
    def exitStatus = jobExecution.getExitStatus()

    then:
    jobInstance.getJobName() == "soJob"
    exitStatus.getExitCode() == ExitStatus.SUCCESS.getExitCode()
  }
}

我无法正确模拟阅读器,我尝试了各种方法,例如更新阅读器的属性,例如 MaxRows,但似乎没有任何效果。

更新阅读器结果的正确方法是什么

或者是否需要以其他方式在单元测试期间正确地操作数据库中的记录



更新:好的,所以我尝试了一种更结构化的方式,使用阅读器内部的服务:

@Component
public class FooDtoItemReader extends AbstractItemStreamItemReader<FooDto> {

  private List<FooDto> foos ;

  private final FooService fooService;

  @Autowired
  public FooDtoItemReader(FooService fooService) {
    this.fooService = fooService;
  }

  @Override
  public void open(ExecutionContext executionContext) {
    try {
      foos = fooService.getFoos();
...
public interface FooService {
  List<FooDto> getFoos();
}
@Service
public class FooServiceImpl implements FooService {

  @Autowired
  private FooDao fooDao;

  @Override
  public List<FooDto> getFoos() {
    return fooDao.getFoos();
  }
}
@Repository
public class FooDaoImpl extends JdbcDaoSupport implements FooDao {

  @Autowired
  DataSource dataSource;

  @PostConstruct
  private void initialize() {
    setDataSource(dataSource);
  }

  @Override
  public List<FooDto> getFoos() {
    return getJdbcTemplate().query(SELECT_SQL, new FooMapper());
  }

}

在这里,我面临无法正确模拟我的服务的问题:

我一定是在测试工具中遗漏了一些东西。

class BatchSOTest extends Specification {

  @Resource
  JobLauncherTestUtils jobLauncherTestUtils

  @Resource
  JobRepositoryTestUtils jobRepositoryTestUtils

  FooService       fooService       = Mock(FooService);
  FooDtoItemReader fooDtoItemReader = new FooDtoItemReader(fooService)

  def cleanup() {
    jobRepositoryTestUtils.removeJobExecutions()
  }

  def "batch init perfectly (second version)"() {
    given:
    fooDtoItemReader.open(Mock(ExecutionContext))

    and:
    // still not working from there :
    (1.._) * fooService.getFoos() >> [createFooEntity(123, "Test")]


    when:
    def jobExecution = jobLauncherTestUtils.launchJob()
    def jobInstance = jobExecution.getJobInstance()
    def exitStatus = jobExecution.getExitStatus()

    then:
    jobInstance.getJobName() == "soJob"
    exitStatus.getExitCode() == ExitStatus.SUCCESS.getExitCode()
  }

但如果我尝试从那里模拟,它会起作用:

class FooDtoItemReaderTest extends Specification {

  FooService fooService = Mock(FooService);
  FooDtoItemReader fooDtoItemReader = new FooDtoItemReader(fooService, 0)

  def "open gets the foos and reader is initialized"() {
    given: "Foos examples"
    def foos = [
      createFooEntity(123, "A"),
      createFooEntity(456, "B")
    ]

    when: "reader is initialized"
    fooDtoItemReader.open(Mock(ExecutionContext))

    then: "service get the expected foos"
    1 * fooService.getFoos() >> foos
  }

那我做错了什么?

【问题讨论】:

    标签: java unit-testing groovy spring-batch spock


    【解决方案1】:

    谈到测试数据库交互时,我不会嘲笑读者。我会改为使用嵌入式数据库并用测试数据填充它。这可以通过将以下 bean 添加到您的测试上下文中来实现:

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("/org/springframework/batch/core/schema-drop-h2.sql")
                .addScript("/org/springframework/batch/core/schema-h2.sql")
                .addScript("/schema.sql")
                .addScript("/test-data.sql")
                .build();
    }
    

    此示例使用 H2,但您可以使用 derby 或 HSLQ 或 SQLite 或任何其他可嵌入数据库。

    【讨论】:

    • 感谢您的回答,我认为这是正确的解决方案。我的读者最初查询了 12 个表。我只需要写入 2 个表,因此我只为这两个表编写了 JPA 实体。在测试启动期间编写每个可以使用jpa.hibernate.ddl-auto: create 启动架构的相应实体还是使用自定义 sql 脚本更好?
    • 我会使用自定义脚本并加载每个测试所需的最少资源。如果您需要更多帮助,请告诉我。如果有帮助,请接受答案:stackoverflow.com/help/someone-answers.
    • 这似乎是个好主意,但我正在处理一些 Oracle 查询,使用包,它似乎与像 H2 这样的可嵌入数据库不兼容,因为我面临很多问题。
    • 在这种情况下,您可以使用像 testcontainers.org 这样的库在容器中运行 oracle 数据库,用测试数据填充它并针对它运行测试。
    • 这似乎是最好的解决方案,保持相同的架构,但它又是一项繁重的工作。现在,我正在尝试使组合起作用:伪造使用复杂 oracle 查询的阅读器,并按照您的建议使用 H2 数据库进行数据操作。我会根据是否有效来更新我的问题。
    猜你喜欢
    • 2021-07-31
    • 2016-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多