Java 单元测试最佳实践
最后更新:2024年5月11日
1. 概述
单元测试是软件设计和实现中的一个关键步骤。
它不仅可以提高代码的效率和有效性,还可以使代码更健壮,并减少未来开发和维护中的回归问题。
在本教程中,我们将讨论一些 Java 单元测试的最佳实践。
2. 什么是单元测试?
单元测试是一种测试源代码是否适合在生产环境中使用的方法论。
我们从通过创建各种测试用例来验证源代码单个单元的行为开始编写单元测试。
然后 完整的测试套件将执行以捕获回归,无论是在实现阶段还是在构建用于各种阶段部署的软件包时,例如预发布和生产环境。
让我们来看一个简单的场景。
首先,让我们创建 Circle 类并在其中实现 calculateArea 方法
public class Circle {
public static double calculateArea(double radius) {
return Math.PI * radius * radius;
}
}
然后,我们将为 Circle 类创建单元测试,以确保 calculateArea 方法按预期工作。
让我们在 src/main/test 目录下创建 CalculatorTest 类
public class CircleTest {
@Test
public void testCalculateArea() {
//...
}
}
在这种情况下,我们使用 JUnit 的 @Test 注解 以及构建工具,例如 Maven 或 Gradle 来运行测试。
3. 最佳实践
3.1. 源代码
将测试类与主源代码分开是一个好主意。因此,它们与生产代码分开开发、执行和维护。
此外,它避免了在生产环境中运行测试代码的任何可能性。
我们可以按照构建工具(例如 Maven 和 Gradle)的步骤,这些工具会在 src/main/test 目录中查找测试实现。
3.2. 包命名约定
我们应该在 src/main/test 目录中为测试类创建类似的包结构,这样可以提高测试代码的可读性和可维护性。
简而言之,测试类的包应该与要测试的源代码类的包匹配。
例如,如果我们的 Circle 类存在于 com.baeldung.math 包中,则 CircleTest 类也应该存在于 src/main/test 目录结构下的 com.baeldung.math 包中。
3.3. 测试用例命名约定
测试名称应该具有洞察力,用户应该仅通过查看名称就能理解测试的行为和期望。
例如,我们的单元测试名称是 testCalculateArea,这对于任何有意义的测试场景和期望信息来说都过于模糊。
因此,我们应该以操作和期望来命名测试,例如 testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble、testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity。
但是,我们仍然可以改进名称以提高可读性。
通常有帮助的是以 given_when_then 的形式命名测试用例,以详细说明单元测试的目的:
public class CircleTest {
//...
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
//...
}
@Test
public void givenDoubleMaxValueAsRadius_whenCalculateArea_thenReturnAreaAsInfinity() {
//...
}
}
我们还应该以 Given、When 和 Then 格式描述代码块。此外,它有助于将测试划分为三个部分:输入、操作和输出。
首先,与给定部分对应的代码块会创建测试对象,模拟数据并安排输入。
接下来,when 部分的代码块代表一个特定的动作或测试场景。
同样,then 部分指出了代码的输出,该输出会使用断言与预期结果进行验证。
3.4. 期望值与实际值
一个测试用例应该在期望值和实际值之间有一个断言。
为了佐证期望值与实际值的概念,我们可以查看 JUnit 的Assert 类中的 assertEquals 方法的定义
public static void assertEquals(Object expected, Object actual)
让我们在我们的一个测试用例中使用这个断言
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(1d);
double expectedArea = 3.141592653589793;
Assert.assertEquals(expectedArea, actualArea);
}
建议在变量名称前加上 “actual” (实际) 和 “expected” (期望) 关键字,以提高测试代码的可读性。
3.5. 偏爱简单的测试用例
在之前的测试用例中,我们可以看到期望值是硬编码的。这是为了避免为了获取期望值而在测试用例中重写或重用实际代码的实现。
不鼓励计算圆的面积来与 calculateArea 方法的返回值进行匹配
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(2d);
double expectedArea = 3.141592653589793 * 2 * 2;
Assert.assertEquals(expectedArea, actualArea);
}
在这种断言中,我们使用相似的逻辑来计算期望值和实际值,从而导致永远产生相似的结果。因此,我们的测试用例对代码的单元测试不会增加任何价值。
因此,我们应该创建一个简单的测试用例,将硬编码的期望值与实际值进行断言。
虽然有时需要在测试用例中编写逻辑,但我们不应该过度这样做。此外,正如常见的,我们绝不应该在测试用例中实现生产逻辑来通过断言。
3.6. 适当的断言
始终使用适当的断言来验证期望值与实际值的对比结果。 我们应该使用 JUnit 的Assert 类或类似框架(如 AssertJ)中提供的各种方法。
例如,我们已经使用了 Assert.assertEquals 方法进行值断言。类似地,我们可以使用 assertNotEquals 来检查期望值和实际值是否不相等。
其他方法,如 assertNotNull、assertTrue 和 assertNotSame 在不同的断言中是有益的。
3.7. 具体的单元测试
不要将多个断言添加到同一个单元测试中,我们应该创建单独的测试用例。
当然,有时验证同一个测试中的多个场景是很有诱惑力的,但最好将它们分开。然后,在测试失败的情况下,将更容易确定哪个特定场景失败了,并且同样,更容易修复代码。
因此,始终编写一个单元测试来测试单个特定的场景。
一个单元测试不会变得过于复杂而难以理解。此外,以后调试和维护单元测试会更容易。
3.8. 测试生产场景
当我们编写测试时考虑到实际场景时,单元测试会更有价值。
原则上,这有助于使单元测试更具相关性。此外,它证明了理解代码在某些生产情况下的行为至关重要。
3.9. 模拟外部服务
尽管单元测试集中在特定的和较小的代码片段上,但代码可能依赖于外部服务来获取某些逻辑的可能性是存在的。
因此,我们应该模拟外部服务,而仅仅测试代码的逻辑和执行,针对不同的场景。
我们可以使用各种框架,如 Mockito、EasyMock 和 JMockit 来模拟外部服务。
3.10. 避免代码冗余
创建更多和更多的 辅助函数来生成常用的对象,并模拟数据或外部服务,用于类似的单元测试。
与其它建议一样,这能增强测试代码的可读性和可维护性。
3.11. 注解
通常,测试框架会提供各种用途的注解,例如执行设置、在运行测试之前执行代码以及在运行测试之后进行清理。
我们可以使用各种注解,例如 JUnit 的 @Before, @BeforeClass 和 @After 以及来自其他测试框架(如 TestNG)的注解。
我们应该 利用注解来为测试准备系统,通过创建数据、安排对象并在每个测试之后删除所有内容,以保持测试用例之间的隔离。
3.12. 80% 测试覆盖率
更多 源代码的测试覆盖率始终是有益的。然而,这并非唯一的目标。我们应该做出明智的决定,并选择适合我们的实现、截止日期和团队的最佳权衡方案。
作为经验法则,我们应该 尝试用单元测试覆盖 80% 的代码。
此外,我们可以使用诸如 JaCoCo 和 Cobertura 之类的工具,以及 Maven 或 Gradle,来生成代码覆盖率报告。
3.13. TDD 方法
测试驱动开发 (TDD) 是一种在持续实现之前和期间创建测试用例的方法。这种方法与设计和实现源代码的过程相结合。
其优势包括 从一开始就可测试的生产代码、具有易于重构和更少回归的健壮实现。
3.14. 自动化
我们可以在创建新构建的同时 通过自动化执行整个测试套件来提高代码的可靠性。
这主要有助于避免在各种发布环境中出现不幸的回归。它还确保在发布损坏的代码之前能够获得快速反馈。
因此,单元测试执行应成为 CI-CD 管道 的一部分,并在出现故障时提醒相关人员。
4. 结论
在本文中,我们探讨了 Java 单元测试的一些最佳实践。遵循最佳实践可以帮助软件开发的许多方面。















