Spring Batch 单元测试实战

Spring Batch 单元测试实战

本文基于之前的 SpringBatch 多写示例,展示如何编写针对性的单元测试。本示例不使用 @SpringBootTest,从而避免启动完整服务上下文;同时,为了简化测试流程,我们对 Reader 进行了 Mock。

在本测试示例中,我们使用了以下 Spring 注解和配置:

  • @EnableBatchProcessing:启用 Spring Batch 所需的核心组件。
  • @TestConfiguration:定义测试专用的配置类。
  • @ContextConfiguration:指定测试加载的配置类。
  • @ExtendWith(SpringExtension.class):激活 Spring 测试支持,使得 Spring 上下文能够在测试中加载。

Job单元测试代码示例

下面代码示例,包含对 JobRepository、JobLauncher、StepBuilderFactory 及 JobBuilderFactory 的模拟配置,以及对 Reader 的 Mock 处理:

java 复制代码
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {MultiWriteTestJobConfiguration.class, Job1Test.MockSpringBatchConfig.class})
@EnableBatchProcessing
public class Job1Test {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job job1;

    @Autowired
    private Job job2;

    @TestConfiguration
    static class MockSpringBatchConfig {

        @Bean
        @Primary
        public JobRepository jobRepository() {
            try {
                return new MapJobRepositoryFactoryBean().getObject();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Bean
        public JobLauncher jobLauncher(JobRepository jobRepository) {
            SimpleJobLauncher launcher = new SimpleJobLauncher();
            launcher.setJobRepository(jobRepository);
            return launcher;
        }

        @Bean
        public StepBuilderFactory stepBuilderFactory(JobRepository jobRepository) {
            return new StepBuilderFactory(jobRepository, new ResourcelessTransactionManager());
        }

        @Bean
        public JobBuilderFactory jobBuilderFactory(JobRepository jobRepository) {
            return new JobBuilderFactory(jobRepository);
        }
    }

    // 使用 @MockBean 对 Reader 进行 Mock,避免依赖实际数据库数据
    @MockBean(name = "myReader")
    private JdbcPagingItemReader<OrderEntity> myReader;

    @BeforeEach
    public void setUp() throws Exception {
        OrderEntity order1 = new OrderEntity();
        order1.setId(1L);
        order1.setAmount(new BigDecimal("1"));
        order1.setChannelId("WEIXIN");

        OrderEntity order2 = new OrderEntity();
        order2.setId(2L);
        order2.setAmount(new BigDecimal("2"));
        order2.setChannelId("WEIXIN");

        OrderEntity order3 = new OrderEntity();
        order3.setId(3L);
        order3.setAmount(new BigDecimal("3"));
        order3.setChannelId("ZHIFUBAO");

        List<OrderEntity> mockOrders = Arrays.asList(order1, order2, order3);

        // 配置 Mock 行为:依次返回三个 OrderEntity 对象,随后返回 null 表示数据读取完毕
        when(myReader.read()).thenReturn(mockOrders.get(0), mockOrders.get(1), mockOrders.get(2), null);
    }

    @Test
    public void testJob() throws Exception {
        JobExecution jobExecution = jobLauncher.run(job2, new JobParameters());
        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

Reader和Writer的单元测试

Reader 单元测试

java 复制代码
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestReaderConfig.class})
public class ReaderTest {

    @Autowired
    private JdbcPagingItemReader<OrderEntity> myReader;

    @Autowired
    private DataSource dataSource;

    @BeforeEach
    public void setUpDatabase() throws Exception {
        // 在内存数据库中创建表,并插入测试数据
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("CREATE TABLE order_table (" +
                    "id BIGINT, " +
                    "channel_id VARCHAR(50), " +
                    "order_no VARCHAR(50), " +
                    "amount DECIMAL(10,2)" +
                    ")");
            stmt.execute("INSERT INTO order_table VALUES (1, 'WEIXIN', 'order1', 1.00)");
            stmt.execute("INSERT INTO order_table VALUES (2, 'ZHIFUBAO', 'order2', 2.00)");
            stmt.execute("INSERT INTO order_table VALUES (3, 'WEIXIN', 'order3', 3.00)");
        }
    }

    @Test
    public void testReader() throws Exception {
        ExecutionContext executionContext = new ExecutionContext();
        myReader.open(executionContext);
        List<OrderEntity> orders = new ArrayList<>();
        OrderEntity order;
        while ((order = myReader.read()) != null) {
            orders.add(order);
        }
        myReader.close();

        // 校验读取到的数据条数及部分数据内容
        assertEquals(3, orders.size());
        assertEquals("WEIXIN", orders.get(0).getChannelId());
        assertEquals(new BigDecimal("2.00"), orders.get(1).getAmount());
    }
}

@Configuration
@Import(MultiWriteTestJobConfiguration.class)
class TestReaderConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }
}

Writer 单元测试

java 复制代码
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestWriterConfig.class})
public class WriterTest {

    @Autowired
    private FlatFileItemWriter<OrderEntity> writer;

    private Path tempFile;

    @BeforeEach
    public void setUp() throws Exception {
        // 创建临时文件作为输出目标
        tempFile = Files.createTempFile("orders_test", ".csv");
        writer.setResource(new FileSystemResource(tempFile.toFile()));
    }

    @Test
    public void testWriter() throws Exception {
        // 准备测试数据
        OrderEntity order = new OrderEntity();
        order.setId(1L);
        order.setChannelId("WEIXIN");
        order.setAmount(new BigDecimal("1"));
        List<OrderEntity> orders = Collections.singletonList(order);

        // 打开 writer 并写入数据
        ExecutionContext executionContext = new ExecutionContext();
        writer.open(executionContext);
        writer.write(orders);
        writer.close();

        // 读取临时文件内容并进行校验
        List<String> lines = Files.readAllLines(tempFile);
        assertFalse(lines.isEmpty());
        String firstLine = lines.get(0);
        assertTrue(firstLine.contains("1"));
        assertTrue(firstLine.contains("WEIXIN"));
        assertTrue(firstLine.contains("1"));
    }
}

@Configuration
@Import(MultiWriteTestJobConfiguration.class)
class TestWriterConfig {

    @Bean
    @Primary
    public FlatFileItemWriter<OrderEntity> testWriter() {
        return new FlatFileItemWriterBuilder<OrderEntity>()
                .name("testOrderItemWriter")
                .resource(new FileSystemResource("target/test-output/orders_test.csv"))
                .delimited()
                .delimiter(",")
                .names("id", "channelId", "amount")
                .build();
    }
}
相关推荐
J_liaty13 小时前
SpringBoot + EMQX:打造物联网设备数据双向通讯的完整解决方案
spring boot·物联网·emqx
Coder_Boy_15 小时前
基于SpringAI的在线考试系统-考试系统DDD(领域驱动设计)实现步骤详解
java·数据库·人工智能·spring boot
crossaspeed16 小时前
Java-SpringBoot的启动流程(八股)
java·spring boot·spring
这儿有个昵称16 小时前
互联网大厂Java面试场景:从Spring框架到微服务架构的提问解析
java·spring boot·微服务·kafka·grafana·prometheus·数据库优化
Coder_Boy_18 小时前
基于SpringAI的在线考试系统-DDD(领域驱动设计)核心概念及落地架构全总结(含事件驱动协同逻辑)
java·人工智能·spring boot·微服务·架构·事件驱动·领域驱动
小北方城市网18 小时前
SpringBoot 集成 RabbitMQ 实战(消息队列解耦与削峰):实现高可靠异步通信
java·spring boot·python·微服务·rabbitmq·java-rabbitmq·数据库架构
程序员老徐18 小时前
SpringBoot嵌入Tomcat注册Servlet、Filter流程
spring boot·servlet·tomcat
guslegend19 小时前
第1章:快速入门SpringBoot
spring boot
Coder_Boy_19 小时前
基于SpringAI的在线考试系统-考试模块前端页面交互设计及优化
java·数据库·人工智能·spring boot
李慕婉学姐20 小时前
Springboot旅游景点管理系统2fj40iq6(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端