作为一家科技公司,快速生产可靠的技术是很重要的。同时,我们不能牺牲代码质量仅仅为了更快地交付。在保持快速发布时间表的同时确保代码质量的主要工具之一是编写测试。和任何其他技能一样,测试编写必须通过实践和经验来发展。
在本文中,我们将使用一个示例程序来探讨代码覆盖率和圈复杂度计算如何有助于确保代码得到充分测试。我们将学习如何使用 JaCoCo 快速获取代码覆盖范围的反馈。最后,我们还将研究代码覆盖的局限性以及即使有 100% 的代码覆盖率,错误也可能会被忽略的情况。
让我们从一个简单的应用程序开始,这是一个 Spring Boot Web 应用程序,用于评估数学表达式。
计算器的界面如下:
业务逻辑如下:
这段代码有一个简单的 HTTP 端点:
应用程序从 Spring Boot 主函数启动:
应用程序的构建由一个简单的 POM 文件控制:
我们也有一个简单的单元测试文件,但它什么都没做:
这段代码的问题在于它没有有用的测试。我们该如何解决这个问题?我们如何知道我们编写的测试是值得编写的?
编写测试时需要考虑一些标准:
- 我们希望确保最好测试的代码部分是最可能包含错误的部分。
- 我们希望将测试重点放在应用程序的关键部分,即可能导致我们的客户获得不良结果的部分。
- 我们不希望编写重复覆盖代码的测试,同时忽略代码的其他部分。
让我们首先尝试找出哪些部分的代码最可能包含错误。如果我们必须对代码中的错误位置进行一般假设,我们会查看最复杂的代码。但是,如何确定哪些代码最复杂?
圈复杂度
一个常见的启发式方法称为圈复杂度。它已经存在很长时间了;Thomas McCabe 在 1976 年发明了它。可以在这里找到算法的简单描述。
- 为方法的开始分配一个点。
- 对于每个条件结构(例如
if
条件),加一分。 - 对于每个迭代结构,加一分。
- 对于
switch
语句中的每个 case 或 default 块,加一分。 - 对于任何其他布尔条件(例如使用
&&
或||
),加一分。
得分越高,方法越复杂。由 McCabe 为国家标准与技术研究所撰写的一篇论文建议将得分保持在 10 或以下。使用圈复杂度时要记住,最终一个人必须声明代码的哪个部分是关键的;任何算法计算的数字只是决策的指南。
NOTE —— 你应该知道 有些人不喜欢使用圈复杂度。
许多公司正在使用 SonarQube 为他们的软件提供代码质量指标。SonarQube 提供的指标之一是圈复杂度。然而,在我看来,它太晚了。SonarQube 通常在已经推送到 git 的代码上运行。它可以监视一个功能分支,但在这种情况下,你需要一个快速的反馈周期,一个不涉及将分支推送到 git 然后等待服务器处理的周期。这就是 JaCoCo 的作用。
介绍 JaCoCo
JaCoCo 是一个开源的 Java 软件质量工具,用于测量代码覆盖率,显示你编写的单元测试覆盖了代码中的哪些行。除了覆盖范围外,JaCoCo 还报告每个方法的复杂度,并告诉你多少复杂度在一个方法中没有得到测试。
让我们看看如何将 JaCoCo 支持添加到我们的计算器服务中。我们只需要在 POM 文件中添加几行即可。在 projects/build/plugins
下添加以下 XML:
在 projects
下添加以下 XML:
现在,你只需要运行命令 mvn test jacoco:report
。这将运行项目中的所有单元测试,并创建代码覆盖信息的 HTML 报告。你可以在项目的 target/site/jacoco
目录中找到此报告。
如果我们查看报告,我们会发现我们缺少了很多:
那是很多红色。在继续之前,让我们了解表格中的列,以便了解我们正在查看和需要改进的内容。
元素列提供了当前应用程序中的包。你可以使用此列深入到代码中,以查看确切的覆盖和未覆盖部分。我们稍后会讨论这一点,但首先我们将看看其他列。
- Missed Instructions and Cov. — 这给出了测试覆盖的 Java 字节码指令数的图形化和百分比度量。红色表示未覆盖,绿色表示已覆盖。
- **Missed Branches and Cov. — **这给出了测试覆盖的 分支 的图形化和百分比度量。一个分支是你代码中的决策点,你需要为每个决策可能采取的每种方式提供(至少)一个测试,以获得完整的覆盖范围。
- Missed and Cxty — 这是源代码的圈复杂度得分。在包级别,这是包中所有类中所有方法的得分之和。在类级别,它是类中所有方法的得分之和,在方法级别,它是方法的得分。
- **Missed and Lines **— 这是代码行数和有多少行没有完全覆盖的数字。
- Missed and Methods — 这是方法数量和没有完全覆盖的方法数量。
- Missed and Classes — 这是类的数量,包括内部类,以及没有至少一些代码覆盖的类的数量。让我们回到Element列。如果你点击一个包名,你会看到一个类似的屏幕,其中包中的类在Element列中。如果你点击
com.example.demo
链接,它会像这样:
如果你点击一个类名,你会看到类中的方法:
最后,如果你点击一个方法的名称,你会看到类的源代码,滚动到该方法:
代码被着色为红色、黄色或绿色,以表示每行是否有完全覆盖、部分覆盖或无覆盖。类名被用绿色突出显示,以显示默认构造函数已被空测试加载Spring应用程序上下文调用。calculator
方法也被调用,因为它的@Bean
注释也将CalculatorImpl
的实例放入了应用程序上下文中。
在包级别上,我们可以看到:
com.example.demo.calculator
包中的覆盖率为0%。com.example.demo.controller
包中的覆盖率为37%。com.example.demo
包中的覆盖率为58%。
我们之所以有任何覆盖率,是因为DemoApplicationTests
中的@SpringBootTest
注释启动了一个Spring应用程序上下文,该上下文加载了构造函数和用@Bean
注释的方法。这证明了一个重要的点;你可以在没有任何测试的情况下触发代码覆盖,但你不应该这样做。在测试中调用代码而不确认调用代码所引起的更改是无效的测试。你可以欺骗Sonar和JaCoCo,但代码评审人员应该验证代码覆盖率反映了实际验证的值。
在JaCoCo中查看单元测试覆盖率
现在我们应该编写一些测试。我们可以从已有的测试DemoApplicationTests
开始。这里没有太多需要验证的内容,但我们可以确保我们加载了正确的业务逻辑实现。在我们的简单程序中,很清楚哪个实现被加载到应用程序上下文中,但是在包含其他人编写的库的较大程序中,你可能会意外地依赖于接口的错误实现。通过类路径扫描,你也可能会错过你认为正在加载的类或REST端点。
这里有一个测试来验证我们是否实例化了正确的对象:
如果我们再次运行mvn test jacoco:report
来进行测试覆盖率,并在DemoApplication
的方法级别上进行详细分析,我们现在看到的是:
好吧,覆盖率报告中什么都没有改变。我们不会为main
添加测试,因为那会启动应用程序,我们不想在单元测试中这样做。但现在我们实际上正在测试确保我们的应用程序正在加载正确的类。记住,类似圆形复杂度和代码覆盖率报告都是帮助人们理解测试和代码质量的工具。最终,人们必须判断测试是否有效。
让我们为我们的REST端点添加一个测试。如果你查看代码,你会注意到虽然有Spring注释将其标记为REST端点、将方法映射到URI,并从请求中提取数据,但你不需要Spring或应用程序上下文来测试该类的业务逻辑。
让我们编写我们的单元测试,而不使用Spring:
由于这是一个单元测试,我们只测试类内的功能;类外的一切(应该)都被替换为模拟实现。我们有一个简单的Calculator
实现,然后有两个测试覆盖了控制器方法的两个可能路径(正常返回值和异常)。
如果我们现在查看我们的代码覆盖率,我们会得到:
进步了!我们的控制器包现在具有100%的代码覆盖率。
现在我们必须为业务逻辑编写测试。显然,这是我们应用程序中最复杂的代码所在的地方。该包的总复杂度为31,其中21点复杂度来自于CalculatorImpl
中的一个单独方法process
。这就是我们应该集中精力的地方。
初始单元测试结果
这些测试中有一些需要注意的事情。首先,在这些测试中再次没有关于Spring的任何内容。通常情况下,你应该避免为测试加载Spring应用程序上下文,因为它会极大地减慢测试速度。
接下来是我们如何通过测试用例。这个业务逻辑针对不同的输入返回不同的输出。不要一遍又一遍地重复自己,使用数据驱动测试来指定预期的输入和预期的输出。JUnit具有内置的参数化支持,使用起来简单,并为每个数据条目输出不同的测试结果。你可以在这里了解更多。
最后,还有负面情况的测试。我们要确保测试的不仅是代码的“黄金路径”。我们还需要了解什么会触发异常,以及会触发什么异常。
在添加这些测试并看到它们通过后,让我们看看我们的代码覆盖率是什么样子的。
这篇文章提到了如何使用 JaCoCo 工具来检查代码覆盖率,并在检查中发现了一些问题。虽然测试用例覆盖了大部分代码,但还是有一些分支未被测试到。文章提醒我们应该编写测试用例来测试复杂的代码块,而不仅仅是简单的代码。此外,代码覆盖率并不是万能的,即使覆盖率接近100%,仍然可能存在漏洞。在文章的最后,作者还提到了重构代码的必要性,并且强调了编写测试用例的重要性。
译自:https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed
评论(0)