[软件测试] sonar 常见问题及修复思路【待完善】
1 sonar 概述
1.1 sonar 是什么?
- Sonar 是一个用于代码质量管理的开放平台。通过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具。
- 与持续集成工具(例如 Hudson/Jenkins 等)不同,Sonar 并不是简单地把不同的代码检查工具结果(例如 FindBugs,PMD 等)直接显示在 Web 页面上,而是通过不同的插件对这些结果进行再加工处理,通过量化的方式度量代码质量的变化,从而可以方便地对不同规模和种类的工程进行代码质量管理。
- 在对其他工具的支持方面,Sonar 不仅提供了对 IDE 的支持,可以在 Eclipse 和 IntelliJ IDEA 这些工具里联机查看结果;同时 Sonar 还对大量的持续集成工具提供了接口支持,可以很方便地在持续集成中使用 Sonar。
1.2 Sonar 插件
1.2.1 SonarLint in IDEA
- IDEA 中的插件叫: SonarLint
安装方式: IDEA > File > Settings > Plugins > "SonarLint"
插件配置路径: IDEA > File > Settings > Tools > SonarLint
2 sonar 常见问题及修复思路
2.1 空指针
- 问题描述
A "NullPointerException" could be thrown; "localAddress" is nullable here.
问题代码[样例]
// 本地(服务器本机)信息
InetSocketAddress localAddress = null;
localAddress = request.getLocalAddress();
String localHostname = null;
localHostname = localAddress.getHostName();
if(StringUtils.isEmpty(localHostname)){
localHostname = "";
}
if(localAddress != null){
log.debug(String.format("[4] request-local-host-info: <host:%s, port:%s>", localHostname, localAddress.getPort()));// <host:0:0:0:0:0:0:0:1, port:18100>
}
- 修复思路
先判断或者先实例化,再访问里面的属性或者成员。
修复代码[样例]
// 本地(服务器本机)信息
InetSocketAddress localAddress = null;
String localHostname = null;
localAddress = request.getLocalAddress();
if(!ObjectUtils.isEmpty(localAddress)){
localHostname = localAddress.getHostName();
log.debug(String.format("[4] request-local-host-info: <host:%s, port:%s>", localHostname, localAddress.getPort()));// <host:0:0:0:0:0:0:0:1, port:18100>
}
if(StringUtils.isEmpty(localHostname)){
localHostname = "";
}
2.2 Optional...orElseThrow()
- 问题描述
The return value of orElseThrow must be used.(必须使用orElseThrow的返回值。)
问题代码[样例]
Optional.ofNullable(object).orElseThrow(() -> {
return new BusinessException("AuthRequestParametersValidator Oject is null!");
});
- 修复思路
我假设这是一个警告(不使用orElseThrow()返回的值不应该是一个错误)。如果您希望消除该警告,请使用isPresent()代替:
if (!itemList.stream().filter(i->orderItemId.equals(i.getId())).findAny().isPresent()) {
throw new BadRequestException("12345","Item Not Found");
}
// 或只是避免使用Optional s,而是使用anyMatch()代替:
if (!itemList.stream().anyMatch(i->orderItemId.equals(i.getId()))) {
throw new BadRequestException("12345","Item Not Found");
}
修复代码[样例]
if(!Optional.ofNullable(object).isPresent()){
throw new BusinessException("AuthRequestParametersValidator Oject is null!");
}
2.3 Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.
- 问题描述
Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation. (添加一个嵌套注释,解释为什么这个方法是空的,抛出一个UnsupportedOperationException异常,或者完成实现。)
- 修复代码(样例)
修复后:
2.4 Add some tests to this class.
- 修复代码(样例)
// 测试类中新增本代码片段
@Test
public void testForSonar(){
Assert.assertTrue(true);
log.info("just for resolve sonar: `Add some tests to this class.`");
}
2.5 Add at least one assertion to this test case.
- 修复代码
// 方法1:在测试方法的最后一行添加断言为真的代码行 | 方法2:用断言真实地判别测试是否通过
@Test
public void xxTest(){
// ...
Assert.assertTrue(true);
}
2.6 Rename this constant name to match the regular expression '[1][A-Z0-9](_[A-Z0-9]+)$'.
- 问题描述
Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.
- 问题分析
共享的编码约定允许团队高效地协作。此规则检查所有**常量名称**是否与提供的**正则表达式**匹配。
其实这里就是检查你起名的规范性,认为定义的常量是遵循'`^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$`' 规则。
> 所以看你需要吧,为了代码整洁统一,后边的开发人员熟悉,可以统一一下
- 修复代码(样例)
小写常量名 --> 大写常量名
//修复前
private static final Long timeout = 5000l;
//修复后
private static final Long timeout = 5000l;
2.7 Remove usage of generic wildcard type.
- 修复代码(样例)
//修复前 : Noncompliant Code
List<? extends Animal> getAnimals(){...}
//修复后 : Compliant Solution
List<Animal> getAnimals(){...}
// or
List<Dog> getAnimals(){...}
3 java.lang.@SuppressWarning
: 抑制/忽略告警
3.1 解释
-
@SuppressWarnings是Java中的一个注解(Annotation),用于抑制编译器产生的警告信息。
-
使用@SuppressWarnings可以告诉编译器忽略特定类型的警告,从而提高代码的可读性和可维护性。这个注解可以应用于类、方法、字段等各种级别的代码上。
-
需要注意的是,使用@SuppressWarnings注解时,应谨慎使用,并且应尽可能地提供具体的警告类型参数,以便更准确地指定要抑制的警告。因为过度使用@SuppressWarnings可能会掩盖真正存在的问题,降低代码的质量。
3.2 案例
3.2.1 unchecked
@SuppressWarnings("unchecked")告诉编译器忽略类型转换警告。这是因为在使用泛型时,编译器通常会产生警告,提示可能会发生类型转换异常。
通过使用@SuppressWarnings注解,我们可以明确告诉编译器我们已经检查过,并确保了类型安全。
@SuppressWarnings("unchecked")
public List<String> getStringList() {
List<String> list = new ArrayList<>();
// 执行一些操作
return list;
}
3.2.2 squid:S3776
@SuppressWarnings("squid:S3776")是Java语言中的一个注解,它被用于抑制一些特定的编译器警告。在这个特定的例子中,"squid:S3776"是一个特定的警告代码,这个警告代码通常来自于SonarQube这个自动化代码审查工具。
具体来说,"squid:S3776"是SonarQube中表示"长方法"的警告代码。长方法指的是那些代码行数过多,可能需要进行重构以提高可读性和可维护性的方法。
3.2.3 all
@SuppressWarnings("all")
4 浅谈:代码质量指标统计
4.1 代码覆盖率
代码覆盖率常被用来作为衡量单元测试好坏的指标。于是,测试人员费尽心思设计案例覆盖代码。用代码覆盖率来衡量,有利也有有弊。本文我们就代码覆盖率展开讨论。
4.1.1 语句覆盖/行覆盖
语句覆盖又称行覆盖,段覆盖,基本块覆盖。这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。
可执行语句
不包括头文件声明、代码注释、空行等等。
只统计可执行的代码
被执行了多少行。需要注意的是,单独一行的花括号{}
也常常被统计进去。
语句覆盖只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。因此测试效果不明显,很难发现代码中的问题。
这里举个栗子,我们看以下被测试代码:
int foo(int a, int b){ return a / b;}
假如我们的测试人员编写如下测试用例:
TeseCase: a = 10, b = 5
Hmm,你可以很自豪地说代码覆盖率达到了100%,并且所有测试用例都Pass了。然鹅我们却没有发现最简单的Bug(当b=0时,会抛出一个除零异常)。
因此,只用语句覆盖率很容易达到所谓的覆盖率,但是并不能保证代码质量。这也反映了几个问题:
- 只使用语句覆盖率来衡量代码质量有问题。
- 测试的目标是保证代码质量,单一的语句覆盖很难做到。
- 是否应该采用更好的测试方法来保证代码质量?
4.1.2 判定覆盖/分支覆盖
判定覆盖又称分支覆盖,所有边界覆盖,基本路径覆盖,判定路径覆盖。
它度量程序中每一个判定的分支是否都被测试到了。判定覆盖非常容易和下面说到的条件覆盖混淆。
因此,我们直接和条件覆盖一起来对比,就明白两者是怎么回事了。
4.1.3 条件覆盖
它度量判定中的每个子表达式结果true和false是否被测试到了。为了说明判定覆盖和条件覆盖的区别,我们来举一个例子,假如我们的被测代码如下:
int foo(int a, int b){
if (a < 10 || b < 10) // 判定 {
return 0; // 分支一
} else {
return 1; // 分支二
}
}
设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,我们设计如下的案例就能达到判定覆盖率100%:
TestCase1: a = 5, b = 任意数字 //覆盖了分支一
TestCase2: a = 15, b = 15 //覆盖了分支二
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,我们设计了如下的案例:
TestCase3: a = 5, b = 5 //true, true
TestCase4: a = 15, b = 15 //false, false
通过上面的例子,我们应该很清楚了判定覆盖和条件覆盖的区别。需要特别注意的是:
- 条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就OK了。
因此,我们可以这样推论:
- 完全的条件覆盖并不能保证完全的判定覆盖。比如上面的例子,假如我设计的案例为:
TestCase5: a = 5, b = 15 //true, false 分支一
TestCase6: a = 15, b = 5 //false, true 分支一
我们看到,虽然我们完整的做到了条件覆盖,但是我们却没有做到完整的判定覆盖,我们只覆盖了分支一。
上面的例子也可以看出,这两种覆盖方式看起来似乎都不咋滴。我们接下来看看第四种覆盖方式。
4.1.4 路径覆盖/断言覆盖
路径覆盖又称断言覆盖。它度量了是否函数的每一个分支都被执行了。
所有可能的分支都要执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,因此,测试路径随着分支的数量指数级别增加。
比如下面的测试代码中有两个判定分支:
int foo(int a, int b){
int nReturn = 0;
if (a < 10) {// 分支一
nReturn += 1;
}
if (b < 10) {// 分支二
nReturn += 10;
}
return nReturn;
}
对上面的代码,我们分别针对我们前三种覆盖方式来设计测试案例:
- 语句覆盖
TestCase a = 5, b = 5 nReturn = 11 //语句覆盖率100%
- 判定覆盖
TestCase1 a = 5, b = 5 nReturn = 11TestCase2 a = 15, b = 15 nReturn = 0//判定覆盖率100%
- 条件覆盖
TestCase1 a = 5, b = 15 nReturn = 1TestCase2 a = 15, b = 5 nReturn = 10//条件覆盖率100%
我们看到,上面三种覆盖率结果看起来都达到了100%!但是上面被测代码中,nReturn的结果一共有四种可能的返回值:0,1,10,11,而我们上面的针对每种覆盖率设计的测试案例只覆盖了部分返回值,因此,可以说使用上面任一覆盖方式,虽然覆盖率达到了100%,但是并没有测试完全。
接下来我们来看看针对路径覆盖设计出来的测试案例:
- 路径覆盖
TestCase1 a = 5, b = 5 nReturn = 0
TestCase2 a = 15, b = 5 nReturn = 1
TestCase3 a = 5, b = 15 nReturn = 10
TestCase4 a = 15, b = 15 nReturn = 11//路径覆盖率100%
路径覆盖将所有可能的返回值都测试到了。这也正是它被很多人认为是“最强的覆盖”的原因了。
还有一些其他的覆盖方式,如:循环覆盖(LoopCoverage),它度量是否对循环体执行了零次,一次和多余一次循环。剩下一些其他覆盖方式就不介绍了。
总结
-
覆盖率只能代表测试过代码,不能代表是否全方位测试;
-
不要过于相信覆盖率数据,它并不能完全衡量代码质量;
-
路径覆盖率 > 判定覆盖 > 语句覆盖
-
测试人员不能盲目追求代码覆盖率,而应该设计更全面的测试用例;
4.2 基于IDEA使用Jacoco插件统计【行覆盖率】、分支覆盖率】
Step1 启用 Jacoco 插件
Step2 显示 branch 覆盖率
参考文献
X 参考文献
-
必须使用“orElseThrow”的返回值(The return value of “orElseThrow” must be used) - 领悟书生
-
The return value of "orElseThrow" must be used - stackoverflow
-
Sonar "Make transient or serializable" error - stackoverflow
-
Sonar 规则 - CSDN 【待整理/待梳理】
-
sonarQube扫描bug、漏洞处理汇总 - CSDN 【待整理/待梳理】
A-Z ↩︎
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!