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

GitLab CI单元测试失败:AnnualReturnRepository.save未被调用排查

GitLab CI单元测试失败问题排查与解决

问题场景

本地环境可正常执行AnnualReturnService的单元测试,但在GitLab CI中运行时失败,报错显示期望调用annualReturnRepository.save(任意AnnualReturn实例),但该Mock对象无任何交互。最初猜测是GitLab无数据库访问权限,排查代码后发现核心原因是业务类中直接实例化OcrUtility,导致测试中Mock的OcrUtility未生效,进而中断了后续save逻辑的执行。

GitLab报错信息

Error message:
[ERROR] Failures: 
[ERROR]   AnnualReturnServiceTest.testSave:102 
Wanted but not invoked:
annualReturnRepository.save(
    <any com.riskanalysisexpertsystem.scanpro.model.company.AnnualReturn>
);
-> at com.test.unit.AnnualReturnServiceTest.testSave(AnnualReturnServiceTest.java:102)
Actually, there were zero interactions with this mock.

相关代码片段

测试类代码(AnnualReturnServiceTest.testSave)

public class AnnualReturnServiceTest{
    
    private Logger logger;
    private PDFtoImageConverterUtility pdftoImageConverterUtility;
    private final String SOURCE_FILE_PATH = "../scanpro/src/test/resources/files/taxdocuments/company/annual_return_form.pdf";
    private final String SOURCE_DESTINATION_PATH = "../scanpro/src/test/resources/files/taxdocuments/company/annual_return.png";
    
    @Mock
    private AnnualReturnRepository annualReturnRepository;
    
    @Mock
    private OcrUtility ocrUtility;
    
    @InjectMocks
    private AnnualReturnService annualReturnService;
    
    @BeforeClass
    public void setUpBeforeClass() throws Exception{
        logger = LogManager.getLogger(AnnualReturnServiceTest.class);
        pdftoImageConverterUtility = new PDFtoImageConverterUtility();
        MockitoAnnotations.openMocks(this);
    }

    public void testSave() {
        logger.info("Mock file and file details");
        
        pdftoImageConverterUtility.convertFile(SOURCE_FILE_PATH, SOURCE_DESTINATION_PATH);
        File file = new File(SOURCE_DESTINATION_PATH);
        String fileDetails = "Annual Return Form\n"
                + "Company Name: Apple Inc.\n"
                + "\n"
                + "TIN: 3243243\n"
                + "\n"
                + "Company Address: 32432 MaryDale Drive., Ghana 234324\n"
                + "Company Registration Number: 432432\n"
                + "Fiscal Year Start Date: 2022-01\n"
                + "\n"
                + "Fiscal Year End Date: 2022-12\n"
                + "\n"
                + "Total Revenue: 342432\n"
                + "\n"
                + "Total Expenses: 3434\n"
                + "\n"
                + "Taxable Income: 34333\n"
                + "\n"
                + "Tax Paid: 3243";
        
        logger.info("Mock the behaviour of OCR utility");
        when(ocrUtility.performOCR(file)).thenReturn(fileDetails);
        
        logger.info("Call the save method");
        annualReturnService.save(file);
        
        logger.info("Verify that parseAnnualReturnDetails is not null");
        try {
            assertNotNull(annualReturnService.parseAnnualReturnDetails(fileDetails));
        } catch (ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        logger.info("Verify the annualReturnRepository's save method is called");
        verify(annualReturnRepository, times(1)).save(any(AnnualReturn.class));
    }
}

业务类代码(AnnualReturnService)

@Service
@RequiredArgsConstructor
public class AnnualReturnService {
    
    private final AnnualReturnRepository annualReturnRepository;

    public void save(File file) {
        
        OcrUtility ocrUtility = new OcrUtility();
        String fileDetails = ocrUtility.performOCR(file);
        
        if (fileDetails != null) {
            
            AnnualReturn annualReturn = new AnnualReturn();
            try {
                annualReturn = parseAnnualReturnDetails(fileDetails);
            } catch (ParseException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            
            if (annualReturn != null) {
                annualReturnRepository.save(annualReturn);
            }
        }
    }
}

.gitlab-ci配置

test:
  stage: test
  script:
    - mvn test -Dtest=com.test.unit.AnnualReturnServiceTest
  allow_failure: true
  dependencies:
    - build
  artifacts:
    paths: 
      - target/surefire-reports
    expire_in: "30 days"

问题根源

业务类AnnualReturnService的save方法中直接通过new OcrUtility()实例化工具类,没有使用依赖注入的方式获取实例。这导致测试类中通过@Mock创建的OcrUtility对象完全没有被业务类使用,业务类实际调用的是真实的OcrUtility实例。在GitLab CI环境中,真实的OcrUtility执行performOCR可能返回null,后续的if (fileDetails != null)判断不成立,导致annualReturnRepository.save()根本不会被调用,最终触发测试失败的报错。

修复方案

  1. 修改业务类,实现OcrUtility的依赖注入
    调整AnnualReturnService的代码,将OcrUtility改为通过构造函数注入:

    @Service
    @RequiredArgsConstructor
    public class AnnualReturnService {
        
        private final AnnualReturnRepository annualReturnRepository;
        private final OcrUtility ocrUtility; // 新增注入的OcrUtility
    
        public void save(File file) {
            
            String fileDetails = ocrUtility.performOCR(file); // 使用注入的实例,移除new操作
            
            if (fileDetails != null) {
                
                AnnualReturn annualReturn = new AnnualReturn();
                try {
                    annualReturn = parseAnnualReturnDetails(fileDetails);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                
                if (annualReturn != null) {
                    annualReturnRepository.save(annualReturn);
                }
            }
        }
    }
    
  2. 优化测试类(可选)
    测试中当前依赖真实的PDF转图片操作,在GitLab环境中可能因文件路径、资源可用性等问题导致不稳定。可以直接Mock File对象,避免真实文件操作:

    // 替换测试中的文件创建逻辑
    File mockFile = Mockito.mock(File.class);
    // 调整OcrUtility的Mock逻辑
    when(ocrUtility.performOCR(mockFile)).thenReturn(fileDetails);
    // 调用save方法时传入mockFile
    annualReturnService.save(mockFile);
    
  3. 验证GitLab CI执行
    提交修改后的代码,重新运行GitLab CI测试,确认报错消失,测试通过。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:45:01