Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >通过单元测试和 JaCoCo 提高 Java 代码覆盖率和质量

通过单元测试和 JaCoCo 提高 Java 代码覆盖率和质量

作者头像
breezedancer
发布于 2019-11-06 09:08:37
发布于 2019-11-06 09:08:37
2.6K00
代码可运行
举报
文章被收录于专栏:技术与生活技术与生活
运行总次数:0
代码可运行

该文章来自https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed 翻译而成(自行解释)

作为一家技术公司,那么公司技术的快速发展是很有必要的。但同时,我们不能为了稍微快一点地交付代码质量而牺牲代码质量。编写测试是保证代码质量,同时保持快速发布计划的主要工具之一。和任何其他技能一样,测试写作必须通过实践和经验来检验。

在本文中,我们将使用一个示例程序来探讨代码覆盖率,以及在循环复杂计算当中如何确保代码正确测试。我们将学习如何使用 JaCoCo 快速获取有关代码覆盖率。最后,我们还将了解代码覆盖率的局限性,即使代码覆盖率达到 100%仍然有bug。

让我们从一个简单的应用程序开始,构建SpringBoot Web项目来来评估计算数学表达式。

项目环境

首先构建一个SPW项目,其中pom为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.ts</groupId>
    <artifactId>mylab</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


<build>
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.1</version>
        <configuration>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
</plugins>
</build>

</project>

接下来编写一个接口

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface Calculator {
    /**
     * 根据字符串,来进行计算结果 比如 “1+1“   那么返回2.0
     * @param expression
     * @return
     */
    double process(String expression)throws CalculatorException;
}

具体的业务逻辑如下,分支计算比较多,为了测试代码代码覆盖率故意为之

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.ArrayDeque;
import java.util.Deque;

public class CalculatorImpl implements Calculator {

    @Override
    public double process(String expression) throws CalculatorException {
        String[] tokens = expression.split(" ");
        Deque<String> operators = new ArrayDeque<>();
        Deque<Double> numbers = new ArrayDeque<>();
        try {
            for (String token : tokens) {
                switch (token) {
                    case "+":
                    case "-":
                    case "/":
                    case "*":
                        while (shouldEvaluate(token, operators.peekFirst())) {
                            String op = operators.pop();

                            double second = numbers.pop();
                            double first = numbers.pop();
                            double result;

                            switch (op) {
                                case "+":
                                    result = first + second;
                                    break;
                                case "-":
                                    result = first - second;
                                    break;
                                case "*":
                                    result = first * second;
                                    break;
                                case "/":
                                    result = first / second;
                                    break;
                                default:
                                    throw new CalculatorException("Unexpected operator " + op);
                            }

                            numbers.push(result);
                        }
                        operators.push(token);
                        break;
                    case "(":
                        operators.push(token);
                        break;
                    case ")":
                        for (String op = operators.peekFirst(); !op.equals("("); op = operators.peekFirst()) {
                            operators.pop();

                            double second = numbers.pop();
                            double first = numbers.pop();
                            double result;

                            switch (op) {
                                case "+":
                                    result = first + second;
                                    break;
                                case "-":
                                    result = first - second;
                                    break;
                                case "*":
                                    result = first * second;
                                    break;
                                case "/":
                                    result = first / second;
                                    break;
                                default:
                                    throw new CalculatorException("Unexpected operator " + op);
                            }

                            numbers.push(result);
                        }
                        operators.pop();
                        break;
                    default:
                        double d = Double.parseDouble(token);
                        numbers.push(d);
                        break;
                }
            }
            for (String op = operators.peekFirst(); op != null; op = operators.peekFirst()) {
                operators.pop();

                double second = numbers.pop();
                double first = numbers.pop();
                double result = 0;

                switch (op) {
                    case "+":
                        result = first + second;
                        break;
                    case "-":
                        result = first - second;
                        break;
                    case "*":
                        result = first * second;
                        break;
                    case "/":
                        result = first * second;
                        break;
                    default:
                        throw new CalculatorException("Unexpected operator " + op);
                }

                numbers.push(result);
            }
        } catch (Exception e) {
            throw new CalculatorException("Invalid expression: " + expression, e);
        }
        double result = numbers.pop();
        if (numbers.size() > 0) {
            throw new CalculatorException("Invalid expression: " + expression);
        }
        return result;
    }

    private boolean shouldEvaluate(String newOp, String topOp) {
        if (topOp == null || topOp.equals("(")) {
            return false;
        }

        // with 4 standard operators, the only time you don't evaluate is
        // when the new operator is a * or / and the top operator is a + or -
        // topOp     	newOp     	shouldEvaluate
        // -----     	----- 	    --------------
        // +, -       	+, -      	true
        // *, /       	+, -      	true
        // +, -       	*, /      	false
        // *, /       	*, /      	true
        if ((topOp.equals("+") || topOp.equals("=")) && (newOp.equals("*") || newOp.equals("/"))) {
            return false;
        }
        return true;
    }
}

编写Controller类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalcController {
    private final Calculator calculator;

    public CalcController(Calculator calculator) {
        this.calculator = calculator;
    }

    @RequestMapping("/")
    public String result(@RequestParam("expression")String expression) {
        try {
            return Double.toString(calculator.process(expression));
        } catch (CalculatorException e) {
            return e.getMessage();
        }
    }
}

最后编写启动类,完成功能开发

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public Calculator calculator() {
        return new CalculatorImpl();
    }
}

测试环境&JaCoCo依赖

接下来我们编写一个测试类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

    @Test
    public void contextLoads() {
    }
}

不过这段测试代码运行完毕后,什么都没有测试到。我们需要增加JaCoCo依赖包,来完成单元测试的覆盖。

pom文件的build节点增加一个插件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.2</version>
  <executions>
    <execution>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
  </executions>
</plugin>

还需要增加reporting节点的内容,如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <reporting>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <reportSets>
                    <reportSet>
                        <reports>
                            <!-- select non-aggregate reports -->
                            <report>report</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
        </plugins>
    </reporting>

覆盖率

好了到此为止,我们的环境Ok了,接下来运行mvn test jacoco:report,最终在target目录生成如下内容

在浏览器中打开index.html,可以看到下面这个图像

有很多红色的线段。在继续之前,让我们回顾一下表中的列,以便了解我们正在寻找什么,以及我们需要改进什么。

第一列,元素列:元素列提供当前应用程序中的包。您可以使用此列向下钻取代码,以准确查看涵盖的内容和未涵盖的内容。我们将在一点一点中介绍这一点,但首先我们将查看其他列。

Missed Instructions :这提供了测试中涵盖的 Java 字节码指令数量的图形和百分比度量。红色表示未覆盖,绿色表示覆盖。

Missed Branches:这给出了测试中涵盖的 [分支] 数量的图形和百分比度量。分支是代码中的决策点,您需要(至少)为决策的每个可能方式提供(至少)测试,以便获得完全覆盖。

Missed & Cxty: 在这里,我们找到您的源代码的循环复杂性分数。在包级别,这是包中所有类中所有方法的分数之和。在类级别,它是类中所有方法的分数总和,在方法级别,它是方法的分数。

Missed & Lines: 这是代码行数和有多少行没有完整的覆盖。

Missed & Methods:这是表示多少方法没有覆盖到。

Missed & Classes:这代表多少类没有覆盖到。

我们点击第一列的包名,一直追溯到启动类的实现,可以发现他的覆盖率是58%。

再深入点击进去,可以看到更加具体的覆盖情况

还可以继续点击方法名称,可以看到里面代码行的覆盖情况

红色的表示没有覆盖到的,绿色表示已经覆盖了。

我们没有写如何的测试代码,但是却有58%的覆盖率,这个是怎么回事呢?原来测试类的注解SpringBootTest会启动一个Spring Application上下文,而这将会加载拥有@Bean注解的方法,并且构造出对象注入到容器中。这说明了一个重要点;您可以触发代码覆盖率,而无需任何测试,但不应该如此。也就是这些测试覆盖率不是真实的覆盖率,需要注意。

那么怎么验证代码实例化呢?

接下来我们完善下测试代码,看看验证实例化是怎么回事:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

    @Autowired
    ApplicationContext ac;

    @Test
    public void contextLoads() {
        Calculator calculator = ac.getBean(Calculator.class);
        assertTrue(calculator instanceof CalculatorImpl);

        CalcController calcController = ac.getBean(CalcController.class);
        assertNotNull(calcController);
    }
}

测试代码如上,还是运行 mvn test jacoco:report

完成之后,代码的覆盖率并没有发生变化,但本质已经不一样了,因为我们现在能确信我们Calculator和CalcController是真实有效的了。

测试Controller方法

目前的CalcController的覆盖率是37%,如下图

我们再测试类中测试一个控制器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
    public void result() {
        CalcController c = new CalcController(new Calculator() {
            @Override
            public double process(String expression) throws CalculatorException {
                if (expression.equals("1 + 1")) {
                    return 2;
                }
                if (expression.equals("+")) {
                    throw new CalculatorException("Invalid expression: +");
                }
                throw new CalculatorException("Unexpected input: "+ expression);
            }
        });
        assertEquals("2.0", c.result("1 + 1"));
        assertEquals("Invalid expression: +", c.result("+"));
    }

再次运行mvn test jacoco:report,得到结果,此时CalcController的覆盖率是100%了

我们的CalculatorImpl的覆盖率太低了,从上图看出。为了增加覆盖率,我们模拟一下测试内容

新增测试类,如下,其中注释的地方有问题,不在测试,只是说明一个问题,需要覆盖所有代码,包括异常

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RunWith(Parameterized.class)
public class CalculatorTest {
    @Parameterized.Parameters(name = "{index}: CalculatorTest({0})={1}, throws {2}")
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {"1 + 1", 2, null},
                {"1 + 1 + 1", 3, null},
//                {"1 – 1", 0, null},
                {"1 * 1", 1, null},
                {"1 / 1", 1, null},
                {"( 1 + 1 )", 2, null},
//                {" + ", 0, new CalculatorException("Invalid expression: +")},
//                {"1 1", 0, new CalculatorException("Invalid expression: 1 1")}
        });
    }

    private final String input;
    private final double expected;
    private final Exception exception;

    public CalculatorTest(String input, double expected, Exception exception) {
        this.input = input;
        this.expected = expected;
        this.exception = exception;
    }

    @Test
    public void testProcess() {
        Calculator c = new CalculatorImpl();
        try {
            double result = c.process(input);
            if (exception != null) {
                fail("should have thrown an exception: " + exception);
            }
            // shouldn't compare doubles without a delta, because FP math isn't accurate
            assertEquals(expected, result, 0.000001);
        } catch (Exception e) {
            if (exception == null) {
                fail("should not have thrown an exception, but threw " + e);
            }
            if (!exception.getClass().equals(e.getClass()) || !exception.getMessage().equals(e.getMessage())) {
                fail("expected exception " + exception + "; got exception " + e);
            }
        }
    }
}

之后运行mvn test jacoco:report 可以看到跟到的代码测试被覆盖到了。

逐步增加测试范围,知道最终代码覆盖率全部为绿色通过为止。

测试是许多开发人员避免做的事情。但是,通过一些简单的工具和对该过程的一些了解,测试可以帮助您减少跟踪 Bug 的时间,将更多时间用于解决有趣的问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 技术谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JaCoCo实例解析之代码覆盖率
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。
软件测试君
2022/05/17
2.8K0
JaCoCo实例解析之代码覆盖率
教你使用 Jacoco 统计服务端代码覆盖率
前面有一篇 文章 使用 Python + Coverage 来统计测试用例的代码覆盖率
AirPython
2020/07/29
3.4K1
教你使用 Jacoco 统计服务端代码覆盖率
使用JaCoCo Maven插件创建代码覆盖率报告
这篇博客文章描述了我们如何使用JaCoCo Maven插件为单元和集成测试创建代码覆盖率报告。
FunTester
2019/11/19
2K0
JaCoCo代码覆盖率从0到100的入门实践
JaCoCo全称是Java Code Coverage,Java代码覆盖率,广泛运用于各种测试平台对Java代码的全量覆盖率和增量覆盖率进行统计,分析代码行差异,度量单元测试效果。Jacoco也是精准测试的技术实现手段之一。
dongfanger
2021/12/24
2.2K0
JaCoCo代码覆盖率从0到100的入门实践
使用dropwizard(4)-加入测试-jacoco代码覆盖率
前言 dropwizard提供了一个简单的测试框架。这里简单集成并加入jacoco测试。 Demo source https://github.com/Ryan-Miao/l4dropwizard 本文是基于dropwizard入门之上的演进。 确保依赖都是最新的,或者自行解决版本冲突,比如jackson不同版本之间的类有所不同。 加入dropwizard-testing 在dependencies中增加依赖 <dependency> <groupId>io.dropwizard</groupId
Ryan-Miao
2018/03/14
1.4K0
Jacoco统计接口测试的代码覆盖率
搜狗商城现有的接口自动化测试框架是使用Python搭建的,共900多条case,每天都会运行一次,从而监控是否有因开发代码变更或者新功能添加而导致的遗漏的bug。但我们只是依照测试用例来转换成自动化脚本、case,实际上并没有度量的指标,也不能保证测试的完整性,所以我们打算引入代码覆盖率这一指标来度量测试完整性。
用户5521279
2019/08/09
3.8K1
Jacoco统计接口测试的代码覆盖率
mock测试及jacoco覆盖率
单元测试是保证项目代码质量的有力武器,但是有些业务场景,依赖的第三方没有测试环境,这时候该怎么做Unit Test呢,总不能直接生产环境硬来吧?
菩提树下的杨过
2020/09/11
4.5K1
使用 JaCoCo 生成测试覆盖率报告
在我们实际的工作中,当完成程序的开发后,需要提交给测试人员进行测试,经过测试人员测试后,代码才能上线到生产环境。
头发还在
2023/10/16
1.2K0
使用 JaCoCo 生成测试覆盖率报告
软件测试|代码覆盖率
JaCoCo,即 Java Code Coverage Library,它由 EclEmma 团队根据多年来使用和集成现有库的经验教训而创建的一个开源的代码覆盖率工具,支持 Java 和 Kotlin;支持计算测试代码对项目的覆盖情况,能定位到测试未覆盖的代码部分;同时它也能检查程序中的废代码和不合理的逻辑提高质量;JaCoCo 能本地进行代码的检查,也可以把它与持续集成工具 Jenkins 进行集成,这样就能在代码提交后自动对提交的代码进行覆盖率的验证,保证提交代码的质量。
霍格沃兹测试开发Muller老师
2022/12/29
8760
ChatGPT与单元测试
这些测试用例覆盖了Calculator类中的所有方法,并且检查了各种情况下的预期行为。
顾翔
2024/09/10
790
ChatGPT与单元测试
SonarQube测试覆盖率--Java
测试覆盖率报告和测试执行报告是评估代码质量的重要指标。测试覆盖率报告告诉您测试用例涵盖的代码百分比。测试执行报告告诉您已运行哪些测试及其结果。
源代码安全
2023/03/01
2.7K0
jacoco 生成单测覆盖率报告
jacoco 是一个开源的覆盖率工具,它针对的开发语言是 java。其使用方法很灵活,可以嵌入到 ant、maven 中;可以作为 Eclipse 插件;可以作为 javaAgent 探针监控 java 程序等等。
JMCui
2020/05/09
3.4K0
jacoco 生成单测覆盖率报告
浅谈代码覆盖率
经常有人问这样的问题:“我们在做单元测试,那测试覆盖率要到多少才行?”。答案其实很简答,“作为指标的测试覆盖率都是没有用处的。”
JavaEdge
2020/05/27
1.8K0
浅谈代码覆盖率
Java代码覆盖率历史发展轨迹
作为一个测试人员,保证产品的软件质量是其工作首要目标,为了这个目标,测试人员常常会通过很多手段或工具来加以保证,覆盖率就是其中一环比较重要的环节。
JavaEdge
2020/05/27
1.3K0
C++语言的单元测试与代码覆盖率
直接交付没有经过测试的代码是不太好的,因为这很可能会浪费整个团队的时间,在一些原本早期就可以发现的问题上。而单元测试,就是发现问题一个很重要的环节。
顾翔
2019/12/11
3.3K0
C++语言的单元测试与代码覆盖率
基于 Jenkins + JaCoCo 实现功能测试代码覆盖率统计
对于 JaCoCo,有所了解但又不是很熟悉。 "有所了解"指的是在 CI 实践中已经使用 JaCoCo 对单元测试代码覆盖率统计: 当代码 push 到代码仓库后,用 JaCoCo 进行单元测试代码覆盖率统计,并将相应数据推送到 SonarQube。 "不是很熟"指的是应用场景也仅限于此,并未进行过多研究与实践。
LinuxSuRen
2019/05/23
4.2K0
Android+jacoco实现代码覆盖率最正确的实现方式,没有之一!
在我接到这个需求,需要统计开发人员提交代码自测率的时候,从其他渠道和gradle推荐了解到的实现方式都是jacoco,然后也上网查了不少的资料,网上的资料都非常老了,gradle插件依赖的不是1.+就是2.+,gradle依赖还是4.4左右,所以导致一个问题,也是浪费了我很多时间的问题:网上的资料已经跟不上时代了,然而没有一篇最新的、最正确的jacoco+Android集成实践的博文,来给有这方面有诉求的同学指引方向,在我费尽千辛万苦终于找到突破口并实现了之后,决定记录这个问题,为日后有需求的同学点一盏明灯!
全栈程序员站长
2022/09/13
3.4K1
Android+jacoco实现代码覆盖率最正确的实现方式,没有之一!
覆盖率检查工具:JaCoCo 食用指南
我们今天简单介绍 JaCoCo 的实际使用示例,它是目前在大多数 Java 项目中应用最广泛的覆盖率检测框架
phoenix.xiao
2022/11/11
1.3K0
覆盖率检查工具:JaCoCo 食用指南
UnitTest:maven中使用Jacoco计算代码覆盖率
jacoco 官网 https://www.eclemma.org/jacoco/
测试邦
2019/08/09
1.5K0
UnitTest:maven中使用Jacoco计算代码覆盖率
Android自动化测试探索(五)代码覆盖率统计
同样如果以上说的几个都不懂也行, 让开发帮忙做这些然后编个代码覆盖率统计的包给你测试, 测完把手机给开发取数据生成报告。 注意每次测试完先返回手机桌面把程序退到后台等几秒让app自己生成日志文件
周希
2019/10/15
1.2K0
相关推荐
JaCoCo实例解析之代码覆盖率
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验