多看看这个,生动
java - Spring、Spring Boot和TestNG测试指南 - 使用Mockito - 颇忒脱 - SegmentFault 思否
如何在Springboot项目中添加testng+mockito+jacoco单元测试_spring testng mockito-CSDN博客
1.介绍
TestNG 和 Mockito 是 Java 测试框架和库,用于编写和运行单元测试以及进行模拟(mocking),它们各有其独特的特点和优势。
TestNG
- 概述: TestNG 是一个功能强大的测试框架,设计灵感来自 JUnit,但它提供了更多功能和灵活性。
- 主要功能:
- 并行测试: TestNG 支持多线程测试,可以并行执行测试,以加快测试速度。
- 依赖测试: TestNG 允许你定义测试的依赖关系。例如,某些测试方法可以依赖其他测试方法,这在有些复杂场景下很有用。
- 数据驱动测试: TestNG 提供了内置的
@DataProvider
注解,可以轻松进行参数化测试。 - 灵活的配置: 通过 XML 配置文件,TestNG 可以非常灵活地配置测试执行,指定不同的测试组,排序测试等。
Mockito
- 概述: Mockito 是一个流行的 Java 库,用于创建和管理测试中的模拟对象(mocks),从而帮助测试单元代码中的依赖。
- 主要功能:
- 模拟对象: 可以创建模拟对象,以便隔离待测试的代码与其依赖。
- 行为验证: 允许你验证方法的调用次数、参数等,以确保代码的逻辑正确。
- 无缝集成: Mockito 可以轻松与 JUnit、TestNG 等测试框架集成,提供灵活的测试功能。
为什么选择 TestNG 和 Mockito 而不是 JUnit?
-
TestNG vs. JUnit:
- 更多功能: TestNG 在依赖管理、多线程测试和灵活配置方面提供了更丰富的功能,这些都是 JUnit 相对较早的版本所不具备的。虽然 JUnit 5 引入了许多新特性,但 TestNG 仍然在特定场景中有其优势。
- 强大的参数化: TestNG 的数据驱动测试(
@DataProvider
)功能比 JUnit 早期的参数化测试更灵活,能更方便地进行复杂场景的测试。 - 测试分组: TestNG 的测试分组功能允许你将测试分类并有选择地运行,这对大型测试套件非常有用。
-
Mockito:
- 易用性: Mockito 提供了简单的 API 来创建和管理 mock 对象,使得单元测试更容易编写和维护。
- 与 TestNG 的集成: Mockito 与 TestNG 无缝集成,能够提供强大的测试功能,尤其是在大型项目中,使用 TestNG+Mockito 可以带来更好的测试体验。
2.单元测试为什么开发做
单元测试由开发人员负责,既是为了保证代码质量,也是为了提高开发效率和协作效果。开发人员最了解他们的代码,因此能够编写最合适的测试来验证代码的功能。通过单元测试,开发人员能够及时发现并修复问题,减少后期的调试和修复成本。
3.单元测试 做什么
单元测试 的主要任务是验证单个功能模块(通常是一个类或方法)是否按照预期工作。它的重点在于隔离代码的最小可测试单元,并对其输入和输出进行测试,以确保其功能的正确性。
单元测试需要做什么?
-
验证功能:
- 确保某个方法或函数在给定输入时,返回预期的输出。
- 测试正常路径(happy path)以及各种异常情况。
-
测试边界条件:
- 测试边界值(如数组的第一个元素或最后一个元素)。
- 检查极端输入(如空值、负数、非常大的数值等)。
-
模拟依赖:
- 如果被测试的单元依赖于其他模块或服务,单元测试应该使用模拟对象来替代这些依赖,以便专注于测试当前单元的行为。
-
测试异常处理:
- 确保方法在遇到异常情况时能够正确处理,例如捕获异常或抛出自定义异常。
-
验证副作用:
- 对方法可能产生的副作用(如修改类的内部状态或调用其他方法)进行验证。
4.TestNG 和 Mockito 负责的部分
TestNG 负责的部分
TestNG 主要负责测试的组织和执行,包括以下功能:
-
组织测试用例:
- 使用
@Test
注解标记测试方法,TestNG 会自动识别和运行这些测试。 - 支持测试用例的分组、排序和依赖管理(通过
dependsOnMethods
、groups
等注解)。
- 使用
-
数据驱动测试:
- 使用
@DataProvider
注解实现数据驱动测试,可以为同一个测试方法提供多组输入数据,从而测试不同的输入组合。
- 使用
-
并行执行:
- TestNG 支持并行执行测试方法,适用于需要在多线程环境下验证代码的场景。
-
设置和清理操作:
- 提供
@BeforeMethod
、@AfterMethod
、@BeforeClass
、@AfterClass
等注解,允许在每个测试方法执行前后或整个测试类执行前后执行特定的设置和清理操作。
- 提供
Mockito 负责的部分
Mockito 主要负责模拟对象和行为验证,特别是在测试需要隔离依赖时:
-
创建模拟对象:
- 使用
Mockito.mock()
方法创建类或接口的模拟对象,这些对象可以代替真实的依赖对象。
- 使用
-
定义行为:
- 通过
when(...).thenReturn(...)
语法,定义在调用模拟对象的方法时返回的结果。 - 使用
thenThrow(...)
模拟方法抛出异常,以测试代码的异常处理逻辑。
- 通过
-
验证方法调用:
- 使用
verify(...)
验证模拟对象的方法是否被调用,以及调用的次数和顺序。 - 例如,验证某个方法是否在某些条件下被正确地调用。
- 使用
-
无缝集成:
- Mockito 可以与 TestNG 或 JUnit 集成,允许在测试框架中直接使用模拟对象进行测试。
5.testNg分层
在 TestNG 中,测试的组织可以通过不同的层次结构来管理和分组。这些层次结构包括Suite、Group、Class 和 Test。下面是这些概念的详细解释及其范围:
理解:分组group是逻辑概念,用来把test层面的进行分组捆绑
suite指一个套件,包含多个class,一个测试类class包含多个测试方法
xml是传统配置,可以直接通过java注解来配置
1. Suite
-
概念: TestNG 的 Suite 是测试的最顶层组织单元,用于将多个测试类组合在一起运行。一个 Suite 通常对应于一个测试运行的配置。
-
定义: 可以通过 XML 配置文件来定义 Suite。在 XML 文件中,你可以指定一系列的测试类或测试方法。
-
范围: Suite 可以包含多个测试类(Class),每个测试类可以包含多个测试方法(Test)。通过 Suite,你可以组织和管理整个测试集。
<suite name="MySuite"> <test name="Test1"> <classes> <class name="com.example.MyTestClass1"/> <class name="com.example.MyTestClass2"/> </classes> </test> </suite>
2. Group
-
概念: Group 是一种将测试方法进行逻辑分组的方式,使得你可以选择性地运行某些组的测试。
-
定义: 在测试方法上使用
@Test(groups = "groupName")
注解来标记测试方法所属的组。在 XML 配置文件中,你可以指定要运行的组。 -
范围: 组的范围通常在 Test 的级别,在同一个 Test 中可以定义多个组,不同的测试方法可以属于不同的组。
@Test(groups = "smoke") public void testMethod1() { // Test code } @Test(groups = "regression") public void testMethod2() { // Test code }
<test name="Test1"> <groups> <run> <include name="smoke"/> <exclude name="regression"/> </run> </groups> <classes> <class name="com.example.MyTestClass"/> </classes> </test>
3. Class
-
概念: Class 是 TestNG 中的基本测试单元之一,它是 Java 类中的一个测试类,用于定义一组相关的测试方法。
-
定义: 你可以在 Java 类中使用
@Test
注解标记测试方法。这些测试方法会被 TestNG 执行。 -
范围: 一个测试类可以包含多个测试方法。TestNG 可以通过 XML 配置文件或注解来指定要运行的测试类。
@Test public class MyTestClass { @Test public void testMethod1() { // Test code } @Test public void testMethod2() { // Test code } }
4. Test
-
概念: Test 是 TestNG 中的一个测试方法或测试用例。它是执行测试逻辑的实际单元。
-
定义: 测试方法通过在 Java 类中使用
@Test
注解来标记。TestNG 会运行这些方法以验证代码的行为。 -
范围: 一个 Test 可以包含多个测试方法。你可以通过 XML 配置文件将多个测试方法组合在一起,也可以使用注解指定测试方法的执行顺序或依赖关系。
@Test public void testMethod1() { // Test code }
总结
- Suite: 最顶层的组织单元,用于组织多个测试类。可以在 XML 配置文件中定义。
- Group: 逻辑上的测试方法分组,用于选择性运行特定的测试组。
- Class: 定义测试方法的 Java 类,一个类可以包含多个测试方法。
- Test: 单个测试方法,测试逻辑的实际执行单元。
通过这些概念,TestNG 提供了灵活的测试组织和执行方式,使得测试管理和执行变得更加高效和可控。
6.测试结果在哪看?什么样子?通过jacoco库来生成测试报告的
编写完代码之后,点小箭头执行测试,执行完毕之后可以在 build/reports/ 目录下找到单元测试报告的html页面。或者控制台也会给出对应的测试结果url地址。
有几个测试,失败多少,忽略多少,测试了多久,成功率
7.src/test测试如何访问src/main下的类
无论是maven还是gradle都会自动进行匹配,
目录结构必须标准,然后你可以在测试里直接像在对应的java类一样使用被测试的东西。
只要路径结构对,这是IDE自动处理的。
testng可以快速生成一个指定类的测试模板,然后你去改
8.对一个新的类或者功能,要做多少单元测试?要做什么方面的?举一个例子
1. 核心功能测试
- 正向测试:确保核心功能在正常输入下表现正确。例如,如果你有一个计算类,测试其基本的加法、减法、乘法和除法。
- 示例:对于一个计算类的
add(int a, int b)
方法,可以编写测试add(2, 3)
并验证结果是否为 5。
2. 边界测试
- 边界条件测试:测试方法的边界输入值,例如最大值、最小值、空值等,确保系统在这些条件下正常工作。
- 示例:如果
add
方法的输入是整型,你可能想测试add(Integer.MAX_VALUE, 1)
,并检查是否正确处理溢出。
3. 异常测试
- 异常情况测试:确保当输入不合法或发生错误时,系统能够抛出正确的异常或返回正确的错误信息。
- 示例:对于一个分母不能为零的除法操作,测试
divide(10, 0)
,并验证它是否抛出ArithmeticException
。
4. 边界情况测试
- 特殊输入测试:处理特殊或极端输入情况,例如空字符串、空集合、零长度数组等。
- 示例:如果你的类有一个方法处理字符串数组,可以测试一个空数组
process(new String[]{})
的行为。
5. 性能测试(可选)
- 性能和效率测试:对于可能涉及大量数据处理或关键路径的功能,可以测试其在大量数据下的性能。
- 示例:对于一个处理大量数据的方法,测试在大输入数据集下的执行时间。
6. 其他特殊场景测试
- 状态变化测试:如果你的类维护了某种内部状态,测试它在不同状态下的行为。
- 示例:如果你的类实现了一个有限状态机,测试状态转换的正确性。
例子:用户注册类的单元测试
假设你有一个 UserRegistration
类,它有以下方法:
public class UserRegistration {
public boolean register(String username, String password) {
// 用户名和密码不能为空
if (username == null || password == null) {
throw new IllegalArgumentException("Username or password cannot be null");
}
// 密码长度必须至少为8个字符
if (password.length() < 8) {
return false;
}
// 注册逻辑
return true;
}
}
针对这个类的单元测试包括:
-
正向测试:
register("user", "strongpassword")
应该返回true
。
-
边界测试:
register("user", "short")
(密码少于8个字符)应该返回false
。register("", "strongpassword")
(空用户名)应抛出IllegalArgumentException
。
-
异常测试:
register(null, "password")
应该抛出IllegalArgumentException
。
-
边界情况测试:
register("user", null)
应该抛出IllegalArgumentException
。
通过这些测试,确保 UserRegistration
类在各种场景下都能正确工作。总体来说,单元测试的目标是覆盖尽可能多的代码路径,确保功能的正确性和鲁棒性。
9.哪些对象应该mock?哪些需要依赖注入?哪些需要new?
如何在Springboot项目中添加testng+mockito+jacoco单元测试_spring testng mockito-CSDN博客
在编写单元测试时,了解何时使用 mock、依赖注入和直接创建对象 (new
) 对于编写高效、可维护的测试代码非常重要。以下是一个指导原则,可以帮助你判断哪些对象应该被 mock,哪些应该依赖注入,哪些可以直接 new
。
1. Mock 对象(外部复杂逻辑的对象,进行隔离)
Mock 对象通常用于模拟外部依赖,以便隔离被测试的代码。以下对象通常需要 mock:
- 外部服务或资源:例如,数据库连接、HTTP 客户端、文件系统操作、消息队列等,这些依赖于外部系统或网络环境,且测试不应依赖这些外部资源。
- 昂贵或复杂的操作:例如,需要大量计算或长时间执行的操作,可以通过 mock 来避免这些实际操作影响测试效率。
- 不确定性的行为:例如,时间相关的操作(获取当前时间)、随机数生成、外部 API 调用,这些行为可能会在不同的测试运行中产生不同的结果。
- 状态不可控的依赖:例如,依赖于用户输入、环境变量等状态的对象。
// Mocking a database repository
@Mock
private UserRepository userRepository;
2. 依赖注入的对象
依赖注入(DI)用于将依赖关系从类的内部移到外部,通常通过构造函数、setter 方法或框架(如 Spring)进行注入。以下对象通常需要依赖注入:
- 可替换的依赖:任何你可能希望在不同环境下替换的依赖,如服务接口、配置对象、策略模式的实现等。
- 可配置的对象:例如,不同的实现类或不同的配置参数,依赖注入允许你在测试和生产环境中使用不同的实现。
- 被 mock 的对象:通常你会使用依赖注入将 mock 对象注入到被测试的类中,以便控制测试中的行为。(比如我mock一个学校,把学校注入年级对象,把年级注入学生对象,测试学生的功能)
// Dependency injection through the constructor
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
3. 直接 new
的对象
直接创建(new
)的对象通常是那些简单、独立且不会产生副作用的对象。以下对象通常可以直接 new
:
- 纯数据对象:如简单的 POJO(Plain Old Java Object),这些对象通常不依赖外部资源或状态,只是封装数据。
- 不可变对象:例如字符串、数字、日期对象,如果它们没有依赖其他服务或状态。
- 轻量级对象:逻辑简单、依赖少、可以在本地测试的对象,这些对象不需要外部注入或 mock。
// Direct instantiation of a simple data class
User user = new User("John", "Doe");
4. 总结
- Mock:外部依赖、昂贵操作、不确定性行为、状态不可控的依赖。
- 依赖注入:可替换的依赖、可配置的对象、需要 mock 的依赖。
new
:纯数据对象、不可变对象、轻量级对象。
举例说明
假设你有一个 OrderService
类,它依赖于 PaymentService
和 InventoryService
:
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public boolean placeOrder(Order order) {
boolean paymentSuccess = paymentService.processPayment(order.getPaymentDetails());
boolean inventorySuccess = inventoryService.reserveInventory(order.getItems());
return paymentSuccess && inventorySuccess;
}
}
在测试 OrderService
时:
PaymentService
和InventoryService
需要 mock:因为它们可能涉及外部系统或复杂逻辑(如支付网关和库存系统)。Order
对象可以直接new
:因为它通常是一个简单的数据结构。OrderService
通过依赖注入获得PaymentService
和InventoryService
:这样你可以在测试中注入 mock 对象。
@Test
public void testPlaceOrderSuccess() {
PaymentService paymentService = mock(PaymentService.class);
InventoryService inventoryService = mock(InventoryService.class);
OrderService orderService = new OrderService(paymentService, inventoryService);
Order order = new Order(...);
when(paymentService.processPayment(order.getPaymentDetails())).thenReturn(true);
when(inventoryService.reserveInventory(order.getItems())).thenReturn(true);
assertTrue(orderService.placeOrder(order));
}
10.代码覆盖率是什么?
覆盖率(Code Coverage)是衡量测试代码对被测试代码覆盖程度的指标,通常用于评估单元测试或集成测试的充分性。覆盖率可以帮助你确定代码的哪些部分已经被测试,哪些部分还没有被测试。
覆盖率的类型
覆盖率可以通过不同的维度来衡量,常见的类型包括:
-
语句覆盖率(Statement Coverage):
- 衡量被测试代码中有多少语句被执行了。
- 例如,如果有 100 行代码,测试执行了 80 行,则语句覆盖率为 80%。
-
分支覆盖率(Branch Coverage):
- 衡量代码中有多少条件分支(如
if-else
、switch
语句)被测试。 - 例如,一个
if
语句包含true
和false
两个分支,分支覆盖率要求测试代码能够使这两个分支都执行一次。
- 衡量代码中有多少条件分支(如
-
条件覆盖率(Condition Coverage):
- 衡量代码中的每个布尔表达式的各个条件是否都被测试过。例如,在
if (a > 5 && b < 10)
中,a > 5
和b < 10
是两个条件,需要分别覆盖。
- 衡量代码中的每个布尔表达式的各个条件是否都被测试过。例如,在
-
路径覆盖率(Path Coverage):
- 衡量所有可能的执行路径是否都被测试过,包括不同的分支组合。这是最全面的一种覆盖率,但在复杂的代码中实现起来难度较大。
-
函数覆盖率(Function Coverage):
- 衡量被测试代码中有多少函数或方法被调用过。
11.单元测试的指标
衡量单元测试质量和效果的指标有以下几种:
-
代码覆盖率(Code Coverage):
- 语句覆盖率:被执行的代码语句比例。
- 分支覆盖率:所有分支路径被测试的比例。
- 条件覆盖率:所有布尔表达式的每个条件是否都被测试。
-
测试通过率(Test Pass Rate):
- 测试用例的通过情况,即通过的测试用例占总用例数的比例。
-
缺陷发现率(Defect Detection Rate):
- 单元测试发现的缺陷数量。高质量的单元测试应能在早期阶段发现大部分缺陷。
-
测试执行时间(Test Execution Time):
- 测试运行所需的时间。短的执行时间有助于频繁的测试迭代。
-
断言数(Number of Assertions):
- 每个测试用例中断言的数量。更多的断言通常表示更详细的验证。
-
测试覆盖范围(Test Coverage Breadth):
- 测试覆盖的功能模块或代码路径的广度,确保所有主要功能都有被测试到。
-
复杂度覆盖率(Complexity Coverage):
- 对复杂代码逻辑或算法的覆盖率,确保复杂逻辑经过充分的测试。
-
Bug 回归率(Bug Regression Rate):
- 测试后重新出现 bug 的比例,低的回归率表示测试的有效性。
12.TestNG和Mockito的注解
1. TestNG 的常用注解
TestNG 是一个强大的测试框架,支持多种类型的测试,如单元测试、集成测试等。以下是 TestNG 中常用的注解及其用途:
-
@Test
:- 表示一个测试方法。被注解的方法将被视为测试用例。
- 你可以通过参数配置测试的优先级、依赖关系、超时时间等。
@Test public void testMethod() { // 测试代码 }
-
@BeforeMethod
:- 在每个测试方法执行之前运行。通常用于准备测试环境,如初始化测试数据或配置。
@BeforeMethod public void setUp() { // 初始化代码 }
-
@AfterMethod
:- 在每个测试方法执行之后运行。通常用于清理资源,如关闭连接、清除缓存等。
@AfterMethod public void tearDown() { // 清理代码 }
-
@BeforeClass
:- 在当前类的所有测试方法之前运行一次。通常用于设置全局资源或配置。
@BeforeClass public void beforeClass() { // 初始化类级别资源 }
-
@AfterClass
:- 在当前类的所有测试方法执行完毕后运行一次。通常用于释放全局资源。
@AfterClass public void afterClass() { // 释放类级别资源 }
-
@BeforeSuite
和@AfterSuite
:- 在整个测试套件开始和结束时分别运行一次。用于配置和清理整个测试环境。
-
@BeforeTest
和@AfterTest
:- 在每个测试标记方法(
@Test
)之前和之后运行。通常用于在每个测试前后设置和清理环境。
- 在每个测试标记方法(
2. Mockito 的常用注解
Mockito 是一个用于创建和管理 mock 对象的流行框架。以下是 Mockito 中常用的注解及其用途:
-
@Mock
:- 用于创建和注入一个 mock 对象。与
Mockito.mock()
方法类似,可以用来模拟接口或类的行为。
@Mock private MyService myService;
- 用于创建和注入一个 mock 对象。与
-
@InjectMocks
:- 自动将标记为
@Mock
的对象注入到需要测试的对象中。常用于依赖注入模式下的类测试。
@InjectMocks private MyController myController;
- 自动将标记为
-
@Spy
:- 创建一个部分 mock 对象,即部分方法真实调用,部分方法模拟。适合测试某些方法而不改变对象的全部行为。
@Spy private MyService myService = new MyServiceImpl();
-
@Captor
:- 用于捕获方法调用时传递的参数,以便进行断言验证。
@Captor private ArgumentCaptor<String> captor;
-
@RunWith(MockitoJUnitRunner.class)
:- 用于在 JUnit 环境下运行 Mockito 注解的测试。这将自动初始化
@Mock
、@InjectMocks
等注解。
- 用于在 JUnit 环境下运行 Mockito 注解的测试。这将自动初始化
@RunWith(MockitoJUnitRunner.class)
public class MyTest {
// 测试代码
}
13.打桩(定义mock对象的行为逻辑和返回)
“打桩”在测试领域通常指的是在软件测试中使用 Stub 或 Mock 来替代真实的组件或依赖,以便隔离测试目标。通过打桩,你可以控制依赖组件的行为,确保测试能够专注于你想测试的部分,而不会受到外部依赖的影响。
Stub 与 Mock
-
Stub:
- Stub 是一种简单的打桩方法,用于在测试中提供预定义的响应而不是调用真实的对象。Stub 通常用于返回固定的值,且不会记录它是否被调用或如何被调用。
- 例如,你可以创建一个 Stub 来返回固定的数据集,而不是从数据库中实际获取数据。
-
Mock:
- Mock 是一种更复杂的打桩方法,它不仅可以替代真实对象,还可以验证它在测试中的调用方式,包括调用的次数、参数等。
- Mock 通常与测试框架(如 Mockito)一起使用,可以设置预期行为,并在测试结束后验证这些行为是否按预期发生。