掌握 Java 中的单元测试和测试驱动开发
通过利用 JUnit 的 Java 单元测试和 TDD,开发人员可以制作出更易于维护和扩展的高质量软件。
单元测试是一种软件测试方法,其中对软件的单个单元或组件进行隔离测试,以检查其运行是否符合预期。在 Java 中,这是一项必不可少的实践,借助它可以尝试验证代码的正确性,并尝试提高代码质量。它基本上可以确保代码工作正常,并且更改不是现有功能的破坏点。
测试驱动开发 (TDD) 是一种测试优先的短迭代软件开发方法。这是一种在编写真正的源代码之前编写测试的做法。它的目标是编写通过预定义测试的代码,因此设计良好、干净且没有错误。
单元测试的关键概念
- 测试自动化:使用用于自动测试运行的工具,例如 JUnit。
- 断言:确认测试中预期结果的语句。
- 测试覆盖范围: 它是测试定义的代码执行百分比。
- 测试套件:测试用例的集合。
- 模拟和存根:模拟真实依赖关系的虚拟对象。
Java 中的单元测试框架:JUnit
JUnit 是一个开源、简单且广泛使用的单元测试。JUnit 是用于单元测试的最流行的 Java 框架之一。换句话说,它附带了编写和运行测试所需的注释、断言和工具。
JUnit的核心组件
1. 注释
JUnit 使用注解来定义测试和生命周期方法。以下是一些关键的注释:
@test
:将一种方法标记为测试方法。@BeforeEach
:表示注释的方法应该在当前类中的每个@Test方法之前执行。@AfterEach
:表示注释的方法应该在当前类中的每个@Test方法之后执行。@BeforeAll
:表示带注释的方法应该在当前类中的任何@Test方法之前执行一次。@AfterAll
:表示注释的方法应该在当前类中的所有@Test方法之后执行一次。@Disabled
:用于临时禁用测试方法或类。
2. 断言
断言用于测试预期结果:
assertEquals
(expected, actual):断言两个值相等。如果不是,则抛出一个。AssertionError
assertTrue
(boolean condition):断言条件为真。assertFalse
(boolean condition):断言条件为 false。assertNotNull
(对象 obj):断言对象不为 null。assertThrows
(Class expectedType, Executable executable):断言执行文件的执行会引发指定类型的异常。
3. 假设
假设与断言类似,但在不同的上下文中使用:
assumeTrue
(布尔条件):如果条件为 false,则测试将终止并被视为成功。assumeFalse
(布尔条件):的倒数。assumeTrue
4. 测试生命周期
JUnit 测试的生命周期从初始化到清理运行:
@BeforeAll
→@BeforeEach
→@Test
→@AfterEach
→@AfterAll
这允许进行正确的设置和拆卸操作,确保测试在干净的状态下运行。
基本 JUnit 测试示例
下面是一个测试基本计算器的 JUnit 测试类的简单示例:
importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.AfterEach;importstaticorg.junit.jupiter.api.Assertions.*;classCalculatorTest{privateCalculator calculator;@BeforeEachvoidsetUp(){
calculator =newCalculator();}@TestvoidtestAddition(){assertEquals(5, calculator.add(2,3),"2 + 3 should equal 5");}@TestvoidtestMultiplication(){assertAll(()->assertEquals(6, calculator.multiply(2,3),"2 * 3 should equal 6"),()->assertEquals(0, calculator.multiply(0,5),"0 * 5 should equal 0"));}@AfterEachvoidtearDown(){// Clean up resources, if necessary
calculator =null;}}
JUnit 5 中的动态测试
JUnit5引入了一个称为动态测试的强大功能。与在编译时使用@Test注释定义的静态测试不同,动态测试是在运行时创建的。这允许在测试创建中具有更大的灵活性和动态性。
为什么要使用动态测试?
- 参数化测试:这允许您创建一组执行相同代码但具有不同参数的测试。
- 动态数据源:基于编译时可能不可用的数据(例如,来自外部源的数据)创建测试。
- 自适应测试:可以根据环境或系统条件生成测试。
创建动态测试
JUnit提供了用于创建动态测试的DynamicTest类。您还需要使用@TestFactory注释来标记返回动态测试的方法。
动态测试示例
importorg.junit.jupiter.api.DynamicTest;importorg.junit.jupiter.api.TestFactory;importjava.util.Arrays;importjava.util.Collection;importjava.util.stream.Stream;importstaticorg.junit.jupiter.api.Assertions.assertEquals;importstaticorg.junit.jupiter.api.DynamicTest.dynamicTest;classDynamicTestsExample{@TestFactoryStream<DynamicTest>dynamicTestsFromStream(){returnStream.of("apple","banana","lemon").map(fruit ->dynamicTest("Test for "+ fruit,()->{assertEquals(5, fruit.length());}));}@TestFactoryCollection<DynamicTest>dynamicTestsFromCollection(){returnArrays.asList(dynamicTest("Positive Test",()->assertEquals(2,1+1)),dynamicTest("Negative Test",()->assertEquals(-2,-1+-1)));}}
创建参数化测试
在JUnit5中,您可以使用@ParameterizedTest注释创建参数化测试。您需要使用特定的源注释来提供参数。以下是常用来源的概述:
@ValueSource
:提供单个文本值数组。@CsvSource
:以 CSV 格式提供数据。@MethodSource
:提供来自工厂方法的数据。@EnumSource
:提供来自枚举的数据。
参数化测试示例
**使用
@ValueSource
**
importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.ValueSource;importstaticorg.junit.jupiter.api.Assertions.assertTrue;classValueSourceTest{@ParameterizedTest@ValueSource(strings ={"apple","banana","orange"})voidtestWithValueSource(String fruit){assertTrue(fruit.length()>4);}}
**使用
@CsvSource
**
importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.CsvSource;importstaticorg.junit.jupiter.api.Assertions.assertEquals;classCsvSourceTest{@ParameterizedTest@CsvSource({"test,4","hello,5","JUnit,5"})voidtestWithCsvSource(String word,int expectedLength){assertEquals(expectedLength, word.length());}}
**使用
@MethodSource
**
importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.MethodSource;importjava.util.stream.Stream;importstaticorg.junit.jupiter.api.Assertions.assertTrue;classMethodSourceTest{@ParameterizedTest@MethodSource("stringProvider")voidtestWithMethodSource(String word){assertTrue(word.length()>4);}staticStream<String>stringProvider(){returnStream.of("apple","banana","orange");}}
参数化测试的最佳实践
- 使用描述性测试名称:利用@DisplayName提高清晰度。
- 限制参数计数:保持参数数量可控,保证可读性。
- 数据提供程序的重用方法:对于@MethodSource,请使用提供数据集的静态方法。
- 合并数据源:使用多个源注释来全面覆盖测试。
JUnit 5 中的标记
JUnit 5 中的另一个突出功能是标记:它允许将自己的自定义标记分配给测试。因此,标记允许对测试进行分组,然后按其标记有选择地执行组。这对于管理大型测试套件非常有用。
标记的主要功能
- 灵活分组:可以将多个标签应用于单个测试方法或类,因此可以定义灵活的分组策略。
- 选择性执行:有时可能需要通过添加标签来仅执行所需的测试组。
- 改进的组织:提供一种有组织的方法来设置测试,以提高清晰度和可维护性。
在 JUnit 5 中使用标签
若要使用标记,请使用@Tag注释注释测试方法或测试类,后跟表示标记名称的字符串。
**
@Tag
的用法示例**
importorg.junit.jupiter.api.Tag;importorg.junit.jupiter.api.Test;@Tag("fast")classFastTests{@Test@Tag("unit")voidfastUnitTest(){// Test logic for a fast unit test}@TestvoidfastIntegrationTest(){// Test logic for a fast integration test}}@Tag("slow")classSlowTests{@Test@Tag("integration")voidslowIntegrationTest(){// Test logic for a slow integration test}}
运行标记测试
您可以使用以下命令使用特定标签运行测试:
- 命令行:通过传递 -t(或 --tags)参数来运行测试,以指定要包含或排除的标记。
mvn test -Dgroups="fast"
- IDE:大多数现代 IDE(如 IntelliJ IDEA 和 Eclipse)都允许通过其图形用户界面选择特定标签。
- 构建工具:Maven 和 Gradle 支持指定要在构建和测试阶段包含或排除的标签。
标记的最佳做法
- 一致的标记名称:在测试套件中对标记使用一致的命名约定,例如“unit”、“integration”或“slow”。
- 分层标记:在类级别应用更广泛的标记(例如,“集成”),在方法级别应用更具体的标记(例如,“慢速”)。
- 避免过度标记:不要在单个测试中添加太多标记,这会降低清晰度和有效性。
JUnit 5 扩展
JUnit 5 扩展模型允许开发人员扩展和以其他方式自定义测试行为。它们提供了一种机制,用于使用附加功能扩展测试、修改测试执行生命周期以及向测试添加新功能。
JUnit 5 扩展的主要功能
- 自定义:修改测试执行或生命周期方法的行为。
- 可重用性:创建可应用于不同测试或项目的可重用组件。
- 集成:与其他框架或外部系统集成,以添加日志记录、数据库初始化等功能。
扩展的类型
- 测试生命周期回调 -
BeforeAllCallback BeforeEachCallback AfterAllCallback AfterEachCallback
- 允许在测试方法或测试类之前和之后执行自定义操作。 - 参数解析器 -
ParameterResolver
.- 将自定义参数注入测试方法,例如模拟对象、数据库连接等。 - 测试执行条件 -
ExecutionCondition
.- 根据自定义条件(例如,环境变量、操作系统类型)启用或禁用测试。 - 异常处理程序 -
TestExecutionExceptionHandler
.- 处理测试执行期间引发的异常。 - 其它 -
TestInstancePostProcessor
、TestTemplateInvocationContextProvider
等。- 自定义测试实例创建、模板调用等。
实现自定义扩展
若要创建自定义扩展,需要实现上述一个或多个接口,并使用 .
@ExtendWith
示例:自定义参数解析程序
将字符串注入测试方法的简单参数解析器:
importorg.junit.jupiter.api.extension.*;publicclassCustomParameterResolverimplementsParameterResolver{@OverridepublicbooleansupportsParameter(ParameterContext parameterContext,ExtensionContext extensionContext){return parameterContext.getParameter().getType().equals(String.class);}@OverridepublicObjectresolveParameter(ParameterContext parameterContext,ExtensionContext extensionContext){return"Injected String";}}
在测试中使用自定义扩展
importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.extension.ExtendWith;@ExtendWith(CustomParameterResolver.class)classCustomParameterTest{@TestvoidtestWithCustomParameter(String injectedString){System.out.println(injectedString);// Output: Injected String}}
扩展的最佳做法
- 关注点分离:扩展应具有单一的、明确定义的职责。
- 可重用性:设计扩展可在不同项目中重用。
- 文档:记录扩展的工作原理及其预期用例。
单元测试和测试驱动开发 (TDD) 提供了显著的好处,对软件开发过程和结果产生积极影响。
单元测试的好处
- 提高代码质量 - 检测 bug:单元测试在开发周期的早期检测 bug,使它们更容易、更便宜地修复。- 代码完整性:测试验证代码更改不会破坏现有功能,从而确保持续的代码完整性。
- 简化重构 - 测试在代码重构过程中充当安全网。如果重构后所有测试都通过,开发人员可以确信重构没有破坏现有功能。
- 文档 - 测试用作实时文档,说明应该如何使用代码。- 它们提供了方法的预期行为的示例,这对新团队成员特别有用。
- 模块化和可重用性 - 编写可测试的代码鼓励模块化设计。- 易于测试的代码通常也更易于重用和理解。
- 减少对变化的恐惧 - 全面的测试套件可帮助开发人员自信地进行更改,因为他们知道如果出现任何问题,他们会收到通知。
- 回归测试 - 单元测试可以捕获回归,其中以前工作的代码由于新的更改而停止正常运行。
- 鼓励最佳实践 - 当单元测试是优先事项时,开发人员倾向于编写更简洁、结构良好和解耦的代码。
测试驱动开发 (TDD) 的优势
- 确保测试覆盖率:TDD 确保生产代码的每一行都至少被一个测试覆盖。这提供了全面的覆盖和验证。
- 关注需求:在编写代码之前编写测试会迫使开发人员在实现之前批判性地思考需求和预期行为。
- 改进设计:TDD的渐进式方法通常会带来更好的系统设计。编写代码时考虑了测试,从而产生了松散耦合和模块化的系统。
- 减少调试时间:由于测试是在代码之前编写的,因此在开发周期的早期就捕获了错误,从而减少了调试所花费的时间。
- 简化维护:经过良好测试的代码更易于维护,因为测试会在引入更改时提供即时反馈。
- 增强开发人员的信心:开发人员对他们的更改更有信心,因为他们知道测试已经验证了他们的代码行为。
- 促进协作:全面的测试套件使多个开发人员能够在同一代码库上工作,从而减少集成问题和冲突。
- 帮助识别边缘情况:在编写测试时考虑边缘情况有助于识别可能被忽略的异常情况。
- 减少总体开发时间:尽管 TDD 最初可能由于编写测试所花费的时间而减慢开发速度,但它通常通过防止错误和减少调试和重构所花费的时间来减少总开发时间。
结论
通过利用 JUnit 的 Java 单元测试和 TDD,开发人员可以制作出更易于维护和扩展的高质量软件。这些做法对于任何专业的软件开发工作流程都是必不可少的,可以增强应用程序代码库的信心和稳定性。
版权归原作者 李憨憨-- 所有, 如有侵权,请联系我们删除。