You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Batch中实现StepExecutionListener的Tasklet单元测试方法咨询

测试实现StepExecutionListener的Spring Batch Tasklet

要对这类Tasklet做纯单元测试,核心是用Mockito模拟Spring Batch的核心对象(比如StepExecution、JobExecution、ExecutionContext),不需要启动Spring容器,直接针对Tasklet的各个方法独立验证逻辑。

依赖准备

确保测试依赖包含:

  • JUnit 5(JUnit Jupiter)
  • Mockito Core
  • Mockito JUnit Jupiter(用于集成Mockito和JUnit)

测试类示例

以下是针对MyTasklet的完整单元测试:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.*;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.scope.context.StepContext;

import java.io.File;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class MyTaskletTest {

    @InjectMocks
    private MyTasklet myTasklet;

    @Mock
    private StepExecution stepExecution;

    @Mock
    private JobExecution jobExecution;

    @Mock
    private ExecutionContext executionContext;

    @Mock
    private StepContribution stepContribution;

    @Mock
    private ChunkContext chunkContext;

    @Mock
    private StepContext stepContext;

    @Test
    void beforeStep_ShouldRetrieveInputFilesFromExecutionContext() {
        // 准备测试数据
        List<File> testFiles = List.of(new File("test1.txt"), new File("test2.txt"));

        // 模拟依赖链
        when(stepExecution.getJobExecution()).thenReturn(jobExecution);
        when(jobExecution.getExecutionContext()).thenReturn(executionContext);
        when(executionContext.get("inputs")).thenReturn(testFiles);

        // 执行方法
        myTasklet.beforeStep(stepExecution);

        // 验证:通过反射获取inputFiles并检查是否正确赋值(因为是私有变量)
        try {
            var inputFilesField = MyTasklet.class.getDeclaredField("inputFiles");
            inputFilesField.setAccessible(true);
            List<File> actualFiles = (List<File>) inputFilesField.get(myTasklet);
            assertEquals(testFiles, actualFiles);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            fail("反射获取inputFiles失败", e);
        }
    }

    @Test
    void execute_ShouldProcessAllInputFiles() throws Exception {
        // 准备测试数据
        List<File> testFiles = List.of(new File("test1.txt"), new File("test2.txt"));
        // 使用Spy来监控MyTasklet,以便验证私有方法调用
        MyTasklet spyTasklet = spy(new MyTasklet());

        // 反射设置inputFiles(跳过beforeStep直接测试execute)
        var inputFilesField = MyTasklet.class.getDeclaredField("inputFiles");
        inputFilesField.setAccessible(true);
        inputFilesField.set(spyTasklet, testFiles);

        // 模拟chunkContext的必要依赖
        when(chunkContext.getStepContext()).thenReturn(stepContext);
        when(stepContext.getStepExecution()).thenReturn(stepExecution);

        // 执行方法
        RepeatStatus result = spyTasklet.execute(stepContribution, chunkContext);

        // 验证结果
        assertEquals(RepeatStatus.FINISHED, result);
        // 验证每个文件都调用了parseFile方法
        verify(spyTasklet, times(testFiles.size())).parseFile(any(File.class));
    }

    @Test
    void execute_ShouldThrowRuntimeExceptionWhenParseFileFails() throws Exception {
        // 准备测试数据
        File testFile = new File("error.txt");
        MyTasklet spyTasklet = spy(new MyTasklet());

        // 反射设置inputFiles
        var inputFilesField = MyTasklet.class.getDeclaredField("inputFiles");
        inputFilesField.setAccessible(true);
        inputFilesField.set(spyTasklet, List.of(testFile));

        // 模拟parseFile抛出异常
        doThrow(new RuntimeException("解析失败")).when(spyTasklet).parseFile(testFile);

        // 模拟chunkContext依赖
        when(chunkContext.getStepContext()).thenReturn(stepContext);
        when(stepContext.getStepExecution()).thenReturn(stepExecution);

        // 验证异常
        RuntimeException exception = assertThrows(RuntimeException.class,
                () -> spyTasklet.execute(stepContribution, chunkContext));
        assertEquals("解析失败", exception.getMessage());
    }

    @Test
    void afterStep_ShouldPutOutputsToExecutionContext() {
        // 模拟依赖链
        when(stepExecution.getJobExecution()).thenReturn(jobExecution);
        when(jobExecution.getExecutionContext()).thenReturn(executionContext);

        // 执行方法
        ExitStatus result = myTasklet.afterStep(stepExecution);

        // 验证结果
        assertEquals(ExitStatus.COMPLETED, result);
        // 验证ExecutionContext是否放入了指定键值对
        verify(executionContext).put("outputs", "results");
    }
}

关键测试要点

  • 模拟Spring Batch核心对象:用Mockito模拟StepExecution、JobExecution等,避免依赖真实的Spring Batch运行环境。
  • 私有变量/方法的处理:
    • 私有变量inputFiles可以通过反射设置或读取,用于验证beforeStep的赋值逻辑。
    • 私有方法parseFile可以通过Mockito的spy对象监控调用情况,或模拟异常场景。
  • 分方法独立测试:分别验证beforeStep、execute、afterStep的逻辑,确保每个方法的职责都被覆盖。
  • 异常场景验证:测试parseFile抛出异常时,execute是否正确抛出RuntimeException。

内容的提问来源于stack exchange,提问作者Eric

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 21:07:09