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

如何单元测试含Lambda与Predicate的Specification函数?

问题描述

我已经尝试为这个包含Lambda和Predicate的Specification函数编写单元测试多日但未能成功,服务里有大量类似函数,导致代码覆盖率大幅下降。想问两个问题:

  1. 有没有办法对这类函数做单元测试?
  2. 如果实在无法测试,能不能配置Jacoco/SonarQube忽略这部分代码?

对应的目标函数代码:

public Specification<ContratEntity> getSpecification(ContratDto dto) {
        List<DepartementEntity> departementEntities = securityService.checkDepartementFiltre();
        String[] filtreBus = securityService.checkBuFiltre();
        List<TypeContratEnum> typeContratEnums = securityService.checkTypeContratFiltre();
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (dto.getNumero() != null && !"".equals(dto.getNumero())) {
                predicates.add(cb.like(cb.lower(root.get("numero")), "%" + dto.getNumero().toLowerCase() + "%"));
            }
            if (dto.getBu() != null && !"".equals(dto.getBu())) {
                predicates.add(cb.like(cb.lower(root.get("bu")), "%" + dto.getBu().toLowerCase() + "%"));
            }

            if (dto.getDepartement() != null && dto.getDepartement().getAbreviation() != null) {
                predicates.add(cb.like(cb.lower(root.get("departement").get("abreviation")), dto.getDepartement().getAbreviation().toLowerCase()));
            }

            if (departementEntities != null) {
                predicates.add(cb.or(
                        root.get("departement").in(departementEntities),
                        root.get("departement").isNull()
                        ));
            }
            if (filtreBus != null) {
                predicates.add(root.get("bu").in(filtreBus));
            }

            if (typeContratEnums != null) {
                predicates.add(root.get("type").in(typeContratEnums));
            }

            if (dto.getLbvFournisseur() != null  && dto.getLbvFournisseur().getCnuf_frn() != null && !dto.getLbvFournisseur().getCnuf_frn().equals("")) {
                predicates.add(cb.or(
                        cb.like(cb.lower(root.join("lbvFournisseur", JoinType.LEFT).get("cnuf_frn")), dto.getLbvFournisseur().getCnuf_frn().toLowerCase()),
                        cb.like(cb.lower(root.join("hlvFournisseur", JoinType.LEFT).get("cnuf_frn")), dto.getLbvFournisseur().getCnuf_frn().toLowerCase()),
                        cb.like(cb.lower(root.join("mlvFournisseur", JoinType.LEFT).get("cnuf_frn")), dto.getLbvFournisseur().getCnuf_frn().toLowerCase())
                ));
            }
            if (dto.getLbvFournisseur() != null && dto.getLbvFournisseur().getRaison_sociale() != null) {
                predicates.add(cb.or(
                        cb.like(cb.lower(root.join("lbvFournisseur", JoinType.LEFT).get("raison_sociale")), "%" + dto.getLbvFournisseur().getRaison_sociale().toLowerCase() + "%"),
                        cb.like(cb.lower(root.join("hlvFournisseur", JoinType.LEFT).get("raison_sociale")), "%" + dto.getLbvFournisseur().getRaison_sociale().toLowerCase() + "%"),
                        cb.like(cb.lower(root.join("mlvFournisseur", JoinType.LEFT).get("raison_sociale")), "%" + dto.getLbvFournisseur().getRaison_sociale().toLowerCase() + "%")
                ));
            }
            if (dto.getLbvFournisseur() != null && dto.getLbvFournisseur().getIce() != null && dto.getLbvFournisseur().getIce() != "") {
                predicates.add(cb.or(
                        cb.like(cb.lower(root.join("lbvFournisseur", JoinType.LEFT).get("ice")), dto.getLbvFournisseur().getIce().toLowerCase()),
                        cb.like(cb.lower(root.join("hlvFournisseur", JoinType.LEFT).get("ice")), dto.getLbvFournisseur().getIce().toLowerCase()),
                        cb.like(cb.lower(root.join("mlvFournisseur", JoinType.LEFT).get("ice")), dto.getLbvFournisseur().getIce().toLowerCase())
                ));
            }
            if (dto.getType() != null) {
                predicates.add(cb.equal(root.get("type"), dto.getType()));
            }
            Predicate[] predicatesArray = new Predicate[predicates.size()];
            query.orderBy(cb.desc(root.get("id")));
            return cb.and(predicates.toArray(predicatesArray));
        };
    }

一、如何对该Specification函数进行单元测试

这类JPA Specification的Lambda函数完全可以测试,核心思路是Mock Specification的三个核心参数(Root、CriteriaQuery、CriteriaBuilder),然后验证Predicate的构建逻辑是否符合预期。

1. 依赖与测试环境准备

确保测试类引入Mockito框架,Spring项目可使用@ExtendWith(MockitoExtension.class)启用Mockito支持。

2. 测试示例代码

@ExtendWith(MockitoExtension.class)
class ContratSpecificationTest {

    @Mock
    private SecurityService securityService;
    @InjectMocks
    private ContratSpecification contratSpecification; // 假设函数所在类为ContratSpecification

    @Mock
    private Root<ContratEntity> root;
    @Mock
    private CriteriaQuery<?> query;
    @Mock
    private CriteriaBuilder cb;
    @Mock
    private Path<String> numeroPath;
    @Mock
    private Expression<String> numeroLowerExpr;
    @Mock
    private Predicate numeroLikePredicate;

    @Test
    void getSpecification_WhenNumeroProvided_AddsLikePredicate() {
        // 1. 构造测试DTO
        ContratDto dto = new ContratDto();
        dto.setNumero("CON123");

        // 2. Mock权限服务返回(按需设置,这里模拟无权限过滤)
        when(securityService.checkDepartementFiltre()).thenReturn(null);
        when(securityService.checkBuFiltre()).thenReturn(null);
        when(securityService.checkTypeContratFiltre()).thenReturn(null);

        // 3. Mock JPA API调用链
        when(root.get("numero")).thenReturn(numeroPath);
        when(cb.lower(numeroPath)).thenReturn(numeroLowerExpr);
        when(cb.like(numeroLowerExpr, "%con123%")).thenReturn(numeroLikePredicate);

        // 4. 执行目标函数并触发Lambda逻辑
        Specification<ContratEntity> spec = contratSpecification.getSpecification(dto);
        spec.toPredicate(root, query, cb);

        // 5. 验证逻辑正确性
        verify(cb).like(numeroLowerExpr, "%con123%");
        verify(query).orderBy(cb.desc(root.get("id")));
    }

    // 针对其他分支(如departement过滤、供应商字段匹配等),复制上述思路编写测试用例即可
}

测试核心要点

  • 每个if分支单独编写测试用例,覆盖参数为空、参数有效、权限过滤生效等所有场景
  • 对于root.join的关联查询场景,需要Mockroot.join("lbvFournisseur", JoinType.LEFT)返回对应的Path对象,再Mock后续字段调用
  • 通过verify方法验证CriteriaBuilder、Root等对象的调用是否符合预期

二、配置Jacoco/SonarQube忽略该部分代码

如果确实不需要测试这类代码,可通过以下方式忽略:

1. Jacoco忽略配置

  • 注解方式:在目标方法上添加@Generated注解(导入javax.annotation.Generated或jakarta.annotation.Generated),Jacoco默认忽略标注该注解的代码:
    @Generated("Specification逻辑,无需单元测试")
    public Specification<ContratEntity> getSpecification(ContratDto dto) {
        // ... 原代码 ...
    }
    
  • Maven配置方式:在jacoco-maven-plugin中添加排除规则:
    <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.8.10</version>
        <configuration>
            <excludes>
                <!-- 排除整个类 -->
                <exclude>**/ContratSpecification.class</exclude>
                <!-- 或排除特定方法 -->
                <exclude>**/ContratSpecification.getSpecification(*)</exclude>
            </excludes>
        </configuration>
    </plugin>
    

2. SonarQube忽略配置

  • 代码注释方式:在目标方法或行前添加// NOSONAR注释:
    // NOSONAR
    public Specification<ContratEntity> getSpecification(ContratDto dto) {
        // ... 原代码 ...
    }
    
  • 配置文件方式:在sonar-project.properties中添加排除规则:
    # 排除整个类
    sonar.exclusions=**/ContratSpecification.java
    # 或排除特定方法的覆盖率统计
    sonar.coverage.exclusions=**/ContratSpecification.java#getSpecification
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 05:35:27