#java #code-coverage #jacoco
#java #покрытие кода #jacoco
Вопрос:
У моей компании есть требование, чтобы мы достигли 90% тестового покрытия для нового кода, и для кода Java я использую плагин gradle jacoco, который хорош; однако процент покрытия филиалов очень сложно повысить до 90%, когда количество филиалов начинает экспоненциально увеличиваться (преувеличивая, вероятно, это геометрический рост).
Вот очень надуманный пример:
public class Application {
public static void test(boolean a, boolean b) {
if (a amp;amp; 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);
}
}
Вот как выглядит отчет о покрытии:
В нем также говорится, что я пропустил 1 из 4 ветвей, или, другими словами, я покрыл 3 из 4 ветвей (75% покрытия ветвей).
Если я увеличу количество логических значений здесь, кажется, что количество ветвей равно n * 2, где n — количество логических значений. Таким образом, 3 (a, b, c) становится 6 ветвями, а 10 становится 20 ветвями. Так что, я думаю, я не понимаю, что значит, что в этом случае должно быть 6 или 20 ветвей.
Чтобы удовлетворить этот вопрос — я мог бы либо
A) Настройте jacoco так, чтобы он был более интуитивным и обрабатывал условия if / else как всегда имеющие 2 ветви (ветвь 1 — это когда выполняется if, а ветвь 2 — когда выполняется else) — отложенное выполнение подвыражений может отслеживаться как покрытие строки или что-то еще.
Б) Чтобы более полно объяснить, почему в нем говорится, что для этих if / else существует 4, 6, 20 ветвей с 2, 3, 10 логическими значениями, объединенными в 1 выражение.
Редактировать — чтобы прояснить, откуда берется путаница:
- Как я охватил 3 ветви в этом примере, когда было всего 2 вызова?
- Почему количество ветвей для 3 логических значений
(a amp;amp; b amp;amp; c)
в примере равно 6 ветвям, а 10 логических значений(a amp;amp; b amp;amp; c amp;amp; .. amp;amp; j)
— 20 ветвям?
Если каждое логическое значение равно true или false, а затем я вызываю функцию с обоими состояниями, почему я не получил здесь 100% покрытия ветвей? Я чего-то не понимаю.
Комментарии:
1. Анализ верен, эта функция реализует поведение для четырех разных случаев. Тот факт, что поведение для трех из них (или, по крайней мере, в настоящее время! ) одинаково, не имеет значения.
2. @jonrsharpe спасибо, но я все еще не понимаю — для 2 логических значений количество ветвей равно 4, что имеет смысл. Но почему тогда 3 логических значения считаются 6 ветвями, а 10 логических значений считаются 20? Кроме того, как я охватил 3 случая, когда я вызывал функцию только 2 раза?
Ответ №1:
Итак, я думаю, что теперь я понял, почему количество ветвей равно n * 2, где n — количество логических выражений внутри условия if ().
Каждое логическое выражение является его собственной ветвью, поэтому в этом примере, если у нас есть a amp;amp; b amp;amp; c
, есть 3 разных выражения, каждое из которых имеет 2 состояния, поэтому 6 ветвей. Чтобы охватить все 6 ветвей, тест должен гарантировать, что каждая переменная оценивается как в истинном, так и в ложном состояниях. Ключевая часть заключается в том, что каждое выражение должно быть вычислено, и в некоторых случаях этого не произойдет из-за отложенной оценки в Java.
public class Application {
public static void test(boolean a, boolean b, boolean c) {
if (a amp;amp; b amp;amp; c) {
System.out.println("true!");
} else {
System.out.println("false!");
}
}
}
Для примера, if (a amp;amp; b amp;amp; c)
когда передается a
, b
и c
значения all true
, это фактически охватывает 3 ветви за одно выполнение. Но если вы передаете все как false
, это охватывает только одну ветвь, потому что b
и c
никогда не проверяются из-за a
ложности и ленивой оценки.
Чтобы эффективно покрыть все 6 ветвей в этом случае, тестовая функция должна вызываться не менее 4 раз для достижения 100% покрытия ветвей.
/*
* 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:
Существующие ответы частично объясняют данный пример, но я хотел бы добавить сюда более общее представление: 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 не может это проверить, поскольку байт-код знает только двоичные условные операторы.
Ответ №3:
В этом примере вам на самом деле нужно всего 3 теста, чтобы получить 100% покрытие. Тестирование случая, когда оба значения false, не обеспечивает никакого дополнительного покрытия. Интуитивно, это должно иметь некоторый смысл. Вы хотите, чтобы он выводил значение true, если хотя бы один из его аргументов не равен false.
То, как вы структурируете код, также влияет на количество ветвей. Если требуется сделать что-то одно, когда все они истинны, и другое, когда любое из них ложно, то вы можете сделать это всего с двумя ветвями:
if (Stream.of(a,b).reduce(Boolean::logicalAnd).get(){
System.out.println("true");
} else {
System.out.println("false");
}
Это выглядит немного глупо в надуманном примере всего с двумя входными данными. С более чем двумя входными данными в реальном контексте это могло бы иметь больше смысла. Например, у вас может быть что-то вроде List<ValidationRule>
, и каждый элемент вычисляет логическое значение. Я не буду говорить намного больше, потому что это выходит за рамки вашего первоначального вопроса, но это может быть чем-то, что стоит рассмотреть.
Ответ №4:
Когда вы пишете условие, подобное if (a amp; amp; b), поэтому при запуске тестового примера оно будет соответствовать всем четырем сценариям, как указано ниже.
result a b
true true true
false false true
false true false
false false false
Итак, вам нужно вызвать этот метод четыре раза, чтобы покрыть 100% coverage
.
Вы также можете создать некоторый служебный класс, который будет генерировать эти сценарии в соответствии с количеством аргументов.
Комментарии:
1. Если бы это было правдой, то почему количество ветвей увеличивается только до 20, когда у меня есть 10 переменных в условии, например
a amp;amp; b amp;amp; c ... amp; j
— Я понимаю, что для 2 переменных вам нужно 2 ^ 2 тестовых примера (4), но почему для 10 переменных мне не нужно 2 ^ 10 (1024) ветвей, и вместо этого говорится, что мне нужно сделать 20 ветвей?2. Также — в моем примере у меня всего 2 вызова теста, и все же каким-то образом я покрыл 3/4 случаев, так что у меня уже есть 75% покрытия только с 2 вызовами, и вы говорите, что мне нужно добавить еще 2 выполнения, чтобы покрыть только одну ветвь?
3. 75% связано с оператором короткого замыкания. Поскольку ваше первое условие равно false, оно не будет проверяться на наличие второго условия, и именно поэтому такой сценарий, как
false,true
get cover. Вы остаетесь сtrue,false
сценарием.4. Почему это называется 4 ветвями, когда кажется, что их всего 3 с объяснением автоматического выключателя 1 = (false amp;amp;?) 2 = (trueamp;amp;false) 3 = (true amp;amp; true), где? не имеет значения, потому что это не оценивается из-за отложенного выполнения? Очевидно, что мне нужно всего 3 (а не 4, как вы сказали) вызова функции, чтобы получить 100% покрытие ветвей.
5. Поскольку общее условие проверяется, здесь 4, но из-за короткого замыкания, которое
false,true
получает покрытие, и вам нужно написать только 1 дополнительное условие.