英文:
Why is Jacoco Coverage Report for Branches saying if (a && b && c) is actually 6 branches?
问题
我的公司有一个要求,即针对新代码,我们的测试覆盖率达到90%。对于Java代码,我正在使用Gradle Jacoco插件,这非常不错;然而,当分支数量呈指数级增长时(夸张地说,可能是几何级增长),将分支覆盖率提高到90%是非常困难的。
以下是一个非常人为的示例:
```java
public class Application {
public static void test(boolean a, boolean b) {
if (a && b) {
System.out.println("true!");
} else {
System.out.println("false!");
}
}
}
以及测试:
public class ApplicationTests {
@Test
public void test() {
Application.test(true, true);
Application.test(false, false);
}
}
覆盖率报告如下:
报告还指出,我遗漏了4个分支中的1个,换句话说,我覆盖了4个分支中的3个(分支覆盖率为75%)。
如果我增加这里的布尔值数量,似乎分支数量是n*2,其中n是布尔值的数量。因此,3个(a、b、c)变为6个分支,10个变为20个分支。所以我想我不理解在这种情况下出现6个或20个分支是什么意思。
为了解决这个问题,我可以:
A)配置Jacoco更直观,将if/else条件始终视为始终具有2个分支(分支1是if执行时,分支2是else执行时)——对子表达式的惰性执行可以作为行覆盖率或其他内容进行跟踪。
B)更完整地解释为什么它说针对这些if/else,用2个、3个、10个布尔值合并为一个表达式时分别有4、6、20个分支。
编辑--为了澄清混淆的来源:
- 在这个示例中,我是如何覆盖了3个分支,而只调用了2次?
- 为什么在示例中,对于3个布尔值(a && b && c),分支数量增加到了6个,而对于10个布尔值(a && b && c && ... && j)却增加到了20个?
如果每个布尔值要么为真要么为假,然后我用这两种状态都调用了函数,为什么我在这里没有达到100%的分支覆盖率?我肯定漏掉了某些东西。
<details>
<summary>英文:</summary>
My company has a requirement that we reach 90% test coverage for new code, and for Java code I am using gradle jacoco plugin which is nice; however, the branch coverage percentage is very difficult to improve to 90% when the number of branches starts increasing exponentially (exaggerating, it's probably geometric growth).
Here is a very contrived example:
public class Application {
public static void test(boolean a, boolean b) {
if (a && b) {
System.out.println("true!");
} else {
System.out.println("false!");
}
}
}
And the test:
public class ApplicationTests {
@Test
public void test() {
Application.test(true, true);
Application.test(false, false);
}
}
Here is what the coverage report looks like:
[![Test Report Screenshot][1]][1]
It also says that I have missed 1 of 4 branches, or in other words I have covered 3 of the 4 branches (75% branch coverage).
If I increase the number of booleans here, it seems the number of branches are n*2 where n is the number of booleans. So 3 (a,b,c) becomes 6 branches, and 10 becomes 20 branches. So I guess I don't understand what it means for there to be 6 or 20 branches in this case.
To satisfy this question - I could either
A) Configure jacoco to be more intuitive and treat if/else conditions as always having 2 branches (branch 1 is when the if executes, and branch 2 is when the else executes) -- lazy execution of sub-expressions could be tracked as line coverage or something else.
B) To explain more completely why it says there are 4, 6, 20 branches for these if/else with 2, 3, 10 booleans combined into 1 expression.
Edit -- to clarify where the confusion comes from:
1. How did I cover 3 branches in this example when there was only 2 calls?
2. Why does the number of branches for 3 booleans `(a && b && c)` in the example go to 6 branches, and 10 booleans `(a && b && c && .. && j)` go to 20 branches?
If each boolean is either true or false, and then I call the function with both states, how did I not get 100% branch coverage here? I am missing something.
[1]: https://i.stack.imgur.com/bImGm.png
</details>
# 答案1
**得分**: 5
我认为我现在已经弄清楚了为什么分支数量等于 n*2 的原因,其中 n 是 if() 条件内部的布尔表达式数量。
每个布尔表达式都是一个分支,所以在这个例子中,如果我们有 `a && b && c`,有 3 个不同的表达式,每个表达式有 2 种状态,所以有 6 个分支。为了覆盖所有 6 个分支,测试必须确保每个变量在真和假两种状态下都进行求值。关键部分在于每个表达式都必须被评估,但在某些情况下,由于 Java 中的惰性求值,它们不会被评估。
对于示例 `if (a && b && c)`,当传递 `a`、`b` 和 `c` 值都为 `true` 时,实际上在单次执行中涵盖了 3 个分支。但如果将它们都传递为 `false`,只涵盖了一个分支,因为由于 `a` 为 false 和惰性求值,不会检查 `b` 和 `c`。
为了在这种情况下高效覆盖所有 6 个分支,必须至少调用 4 次测试函数以实现 100% 的分支覆盖。
```java
/*
* 使用 ? 表示它可以是 true 或 false,
* 这无关紧要,因为由于惰性求值,变量实际上永远不会被读取。
*/
Application.test(true, true, true); // 涵盖 +3 个分支
Application.test(true, true, false); // 涵盖 +1 个分支
Application.test(true, false, ?); // 涵盖 +1 个分支
Application.test(false, ?, ?); // 涵盖 +1 个分支
// 总共:6 个分支
英文:
So I think I've figured out now why the reason the number of branches is equal to n*2 where n is the number of boolean expressions inside the if() condition.
Each boolean expression is its own branch, so in this example if we have a && b && c
there are 3 different expressions each with 2 states so 6 branches. To cover all 6 branches, the test must ensure each variable is evaluated in both true and false states. The key part is that each expression must be evaluated, and in some cases they won't be because of lazy evaluation in Java.
public class Application {
public static void test(boolean a, boolean b, boolean c) {
if (a && b && c) {
System.out.println("true!");
} else {
System.out.println("false!");
}
}
}
For the example if (a && b && c)
when passing a
, b
, and c
values all true
, this actually covers 3 branches in a single execution. But if you pass all as false
, it only covers one branch because b
and c
are never checked due to a
being false and lazy evaluation.
To efficiently cover all 6 branches in this case, the test function must be called no less than 4 times to achieve 100% branch coverage.
/*
* Using ? to indicate it can be true or false,
* it won't matter because the variable would never be read due to lazy evaluation.
*/
Application.test(true, true, true); // +3 branches covered
Application.test(true, true, false); // +1 branch covered
Application.test(true, false, ?); // +1 branch covered
Application.test(false, ?, ?); // +1 branch covered
// total: 6 branches
答案2
得分: 2
现有的答案在一定程度上解释了给定的示例,但我想在这里添加一个更一般的观点:Jacoco分析字节码,分支覆盖仅仅计算二进制条件语句(分支)的目标数量。
在上面的示例中,但有三个变量时,我们会得到6个分支。
观察字节码,我们可以看到短路运算符被翻译成了三个ifeq
,它们代表分支语句。每个分支有两个可能的目标,总共有六个。
public static void test(boolean, boolean, boolean);
Code:
0: iload_0
1: ifeq 23
4: iload_1
5: ifeq 23
8: iload_2
9: ifeq 23
12: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #22 // String true!
17: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: goto 31
23: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #30 // String false!
28: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: return
如何实现全覆盖可以在相应的控制流图中看到:第一个测试用例test(true,true,true)
沿着顶部的路径,覆盖了三个分支。对于剩余的分支,我们需要另外三个测试用例,每个测试用例在短路运算符处选择另一个“退出”。
所需的用于实现100%分支覆盖的测试用例数量不会随着条件中子表达式数量的增加而呈指数增长 - 实际上是线性增长的。然而,要求覆盖所有可能的子条件组合(在这里是a、b和c的值)被称为多条件覆盖(维基百科:覆盖率)。然而,Jacoco无法检查这一点,因为字节码只知道二进制条件语句。
英文:
The existing answers partly explain the given example, but I would like to add a more general view here: Jacoco analyzes bytecode, and the branch coverage merely counts the targets of the binary conditional statements (branches) within.
Given the example above, but with three variables, we get 6 branches.
Looking at the bytecode we see that the short circuit operators are translated to three ifeq
, which represent branch statements. Each of them has two possible targets, makes six altogether.
public static void test(boolean, boolean, boolean);
Code:
0: iload_0
1: ifeq 23
4: iload_1
5: ifeq 23
8: iload_2
9: ifeq 23
12: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #22 // String true!
17: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: goto 31
23: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #30 // String false!
28: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: return
How to obtain full coverage can be seen in the corresponding control flow graph: The first test case test(true,true,true)
takes the path along the top, covering three of the branches. For the remaining branches we need three more test cases, each taking another "exit" at the short circuit operators.
The required test cases for 100% branch coverage do not grow exponentially with the number of sub-expressions in the condition - in fact it's linear. Requiring to cover all possible combinations of sub-conditions (here: the values of a, b and c), however, is called multiple condition coverage (wikipedia:coverage). Jacoco can not check that as the bytecode only knows binary conditional statements.
答案3
得分: 1
在这个示例中,实际上只需要3个测试就可以达到100%的覆盖率。测试同时为假的情况并不会提供任何额外的覆盖率。直观地说,这应该是有一定道理的。你希望它在至少有一个参数为假时才打印true。
你构造代码的方式也会影响分支的数量。如果要求在所有参数都为真时执行一件事情,在任何一个参数为假时执行另一件事情,那么你只需要两个分支:
if (Stream.of(a, b).reduce(Boolean::logicalAnd).get()) {
System.out.println("true");
} else {
System.out.println("false");
}
在一个虚构的只有两个输入的示例中,它看起来有点愚蠢。但在实际情况下,如果有两个以上的输入,那么它可能会更有意义。例如,你可以有一个类似List<ValidationRule>
的结构,每个元素计算一个布尔值。我不会多说,因为这已经超出了你最初问题的范围,但这可能值得考虑。
英文:
In this example, you actually only need 3 tests to get 100% coverage. Testing the case when both are false doesn't provide any additional coverage. Intuitively, this should make some sense. You want it to print true unless at least one of its arguments is false.
The way you structure the code impacts the number of branches, too. If the requirement is to do one thing then when all of them are true and another when any one of them is false then you can do it with just two branches:
if (Stream.of(a,b).reduce(Boolean::logicalAnd).get(){
System.out.println("true");
} else {
System.out.println("false");
}
It looks kind of silly in a contrived example with just two inputs. With more than two inputs in an actual context, then it could make more sense. For example you could have something like a List<ValidationRule>
and each element computes a boolean value. I won't say much more because it's beyond the scope of your original question, but it could be something worth considering.
答案4
得分: 0
当您编写类似于 if(a && b) 的条件时,在运行测试用例时,它将根据下面提到的四种情况运行。
结果 a b
真值 真值 真值
假值 假值 真值
假值 真值 假值
假值 假值 假值
因此,您需要调用此方法四次以覆盖**100%覆盖率
**。
您还可以创建一些实用类,根据参数数量生成这些场景。
英文:
When you are writing condition like if(a && b) so while running test case it will goes with all four scenarios as mentioned below.
result a b
true true true
false false true
false true false
false false false
So you need to call this method four times to cover 100% coverage
.
You can also create some utility class which will generate these scenario according to number of arguments.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论