paint-brush
Java 测试:关键概念 [第 1 部分:单元测试]经过@gromspys
1,418 讀數
1,418 讀數

Java 测试:关键概念 [第 1 部分:单元测试]

经过 Sergei Korneev10m2023/11/14
Read on Terminal Reader

太長; 讀書

在本文中,我们将探讨 Java 单元测试的关键概念,包括简单的代码示例、参数化、异常测试和注释。
featured image - Java 测试:关键概念 [第 1 部分:单元测试]
Sergei Korneev HackerNoon profile picture

单元测试是软件开发中的基本实践,可确保代码的可靠性和正确性。在本文中,我们将探讨 Java 单元测试的关键概念,包括简单的代码示例、参数化、异常测试、 @Before@BeforeEach@After@AfterEach等注释,以及模拟和存根的使用。


我们还将介绍流行的 Mockito 模拟库,并提供使用 Maven 插件测量代码覆盖率的指南。

单元测试的重要性

单元测试是单独测试各个组件或代码单元以验证其正确性的做法。主要目标是在开发过程的早期检测并修复错误,确保每个代码单元的行为符合预期。


让我们首先创建一个可用于示例的简单计算器类。 Calculator类包含基本算术运算:

 public class Calculator { public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } public int multiply(int a, int b) { return a * b; } public int divide(int a, int b) { return a / b; } }

编写简单的单元测试

要为Calculator类创建单元测试,您可以使用 JUnit 框架,这是一种流行的 Java 测试框架。让我们为add方法编写一个简单的单元测试:

 import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { @Test public void testAdd() { Calculator calculator = new Calculator(); int result = calculator.add(2, 3); assertEquals(5, result); } }

在此测试中,我们导入必要的 JUnit 类并使用@Test注解测试方法。然后,我们创建Calculator类的实例并断言add方法的结果等于预期值 (5)。

参数化测试

参数化测试允许您使用多组输入数据运行相同的测试逻辑。这对于测试具有各种输入值的方法很有用。要在 JUnit 中执行此操作,您可以使用@ParameterizedTest注释并提供输入值和预期结果作为参数。这是一个例子:

 import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { @ParameterizedTest @CsvSource({"2, 3, 5", "4, 7, 11", "0, 0, 0"}) public void testAdd(int a, int b, int expected) { Calculator calculator = new Calculator(); int result = calculator.add(a, b); assertEquals(expected, result); } }

在此示例中, @ParameterizedTest注释允许我们以 CSV 格式提供多组输入值和预期结果。对每个组合都执行测试方法,保证add方法的正确性。

异常测试

测试异常对于确保您的代码正确处理错误至关重要。要测试异常情况,您可以使用@Test注释以及 JUnit 提供的assertThrows方法。


这是一个例子:

 javaCopy codeimport org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; public class CalculatorTest { @Test public void testDivideByZero() { Calculator calculator = new Calculator(); assertThrows(ArithmeticException.class, () -> calculator.divide(5, 0)); } }

在此测试中,我们验证Calculator类中的数字除以零会引发ArithmeticException

使用注释:@Before、@BeforeEach、@After 和 @AfterEach

@Before@BeforeEach@After@AfterEach等注释用于设置和拆除测试环境。这些注释有助于管理常见的测试初始化和清理任务。


  • @Before@After分别在测试类中的所有测试方法之前和之后运行一次。
  • @BeforeEach@AfterEach在每个测试方法之前和之后运行。


这是一个例子:

 import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { private Calculator calculator; @BeforeEach public void setUp() { calculator = new Calculator(); } @AfterEach public void tearDown() { calculator = null; } @Test public void testAdd() { int result = calculator.add(2, 3); assertEquals(5, result); } @Test public void testSubtract() { int result = calculator.subtract(5, 3); assertEquals(2, result); } }

在此示例中, setUp方法使用@BeforeEach进行注释,并在每次测试之前初始化Calculator对象。使用@AfterEach注释的tearDown方法会在每次测试后清理资源。

模拟和存根

当您想要将正在测试的代码与外部依赖项(例如数据库或 Web 服务)隔离时,模拟和存根在单元测试中至关重要。它们允许您模拟这些依赖项的行为。


  • 模拟:模拟是模仿真实对象行为但受您控制的对象。它记录交互并允许您验证是否调用了特定方法。


  • 存根:存根提供对方法调用的预设响应。它返回预定义的数据并用于模拟外部系统的某些行为。


让我们扩展Calculator类以包含涉及外部依赖项的方法,然后创建一个存根来模拟该依赖项。

 public class Calculator { private ExternalService externalService; public Calculator(ExternalService externalService) { this.externalService = externalService; } public int performComplexCalculation(int a, int b) { int result = externalService.multiply(a, b); return result + externalService.getConstant(); } }

在此更新的Calculator类中, performComplexCalculation依赖于ExternalService来进行乘法并获取常量值。


以下是您如何创建一个存根来测试Calculator类,而不依赖于实际的ExternalService

 import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; public class CalculatorTest { @Test public void testPerformComplexCalculation() { // Create a stub for ExternalService ExternalService externalServiceStub = new ExternalService() { @Override public int multiply(int a, int b) { return a * b; } @Override public int getConstant() { return 5; // Stubbed constant value } }; // Create the Calculator with the stubbed ExternalService Calculator calculator = new Calculator(externalServiceStub); // Test the performComplexCalculation method int result = calculator.performComplexCalculation(2, 3); assertEquals(11, result); // 2*3 + 5 (constant) = 11 } }

在此测试中, ExternalService通过创建覆盖必要方法的匿名类在测试方法中进行存根。这样, Calculator测试方法的运行无需依赖于ExternalService的实际实现。


存根可用于模拟外部系统或依赖项的行为,以隔离和测试类或方法的特定功能。这允许您控制存根的行为并专注于被测试的单元,而不需要实际的外部服务。

介绍用于模拟的 Mockito

Mockito 是一个流行的 Java 库,用于创建和管理模拟对象。假设我们有一个与外部支付网关交互的PaymentService类。我们可以使用 Mockito 为支付网关创建模拟:

 import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; public class PaymentServiceTest { @Test public void testProcessPayment() { PaymentGateway paymentGateway = mock(PaymentGateway.class); PaymentService paymentService = new PaymentService(paymentGateway); when(paymentGateway.charge(100.0)).thenReturn(true); boolean result = paymentService.processPayment(100.0); assertTrue(result); verify(paymentGateway, times(1)).charge(100.0); } }

在此示例中,我们创建一个模拟PaymentGateway并使用when方法定义其行为。然后,我们调用PaymentService类上的processPayment方法,并验证是否使用预期参数调用了charge方法。

代码覆盖率和 Maven 插件

代码覆盖率衡量单元测试执行的代码行、分支或语句的百分比。它可以帮助您识别未经测试的代码和可能需要额外测试的区域。


Maven 是一种流行的 Java 项目构建工具,您可以使用 JaCoCo 等插件将代码覆盖率分析集成到 Maven 项目中。以下是将 JaCoCo 添加到您的项目中的方法:


  1. 打开项目的pom.xml文件。
  2. 添加 JaCoCo Maven 插件配置:
 <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.7</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> </plugins> </build>


使用以下命令运行 Maven 构建:

 mvn clean verify


运行构建后,您可以在target/site/jacoco/index.html文件中找到代码覆盖率报告。此报告提供有关单元测试的代码覆盖率的详细信息。

结论

Java 单元测试是确保代码可靠性和正确性的重要实践。使用 JUnit 和 Mockito 等工具,您可以为组件编写有效的单元测试和模拟。


通过将代码覆盖率分析与 Maven 和 JaCoCo 集成,您可以确保您的测试覆盖代码库的很大一部分。定期测试和维护单元测试将帮助您生成高质量和健壮的 Java 应用程序。


在本系列的下一部分中,我们将探讨集成测试,这是软件测试的一个关键方面,涉及测试不同组件和服务之间的交互,以确保整个系统正常运行。


请继续关注第二部分,我们将深入探索令人兴奋的集成测试世界!