如何单元测试含Lambda与Predicate的Specification函数?
问题描述
我已经尝试为这个包含Lambda和Predicate的Specification函数编写单元测试多日但未能成功,服务里有大量类似函数,导致代码覆盖率大幅下降。想问两个问题:
- 有没有办法对这类函数做单元测试?
- 如果实在无法测试,能不能配置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
相关产品推荐
相关产品推荐

