知识点
单元测试的定义
- 单元测试(Unit Testing)是一种软件开发的验证过程,旨在隔离并检测软件组件(通常是函数、方法或类)的单个单元的功能是否按照预期执行。每个测试用例验证特定的条件或功能,确保代码的每个部分都能独立地按预期工作。
- 单元测试与集成测试、系统测试的区别。
-
集成测试(Integration Testing):
- 目的:验证多个单元或组件在一起工作时的交互和协作是否正确。
- 范围:通常在完成单元测试后进行,关注组件间的接口和交互。
- 特点:可能需要模拟外部系统或使用实际的外部系统。
-
系统测试(System Testing):
- 目的:验证整个系统的功能是否满足用户需求和系统规格。
- 范围:在所有集成测试完成后进行,涵盖整个系统。
- 特点:测试用例基于用户需求和系统规格,可能包括性能测试、安全性测试等。
-
接受测试/验收测试(Acceptance Testing):
- 目的:确保软件满足业务需求和用户验收标准。
- 范围:通常在系统测试之后,由用户或用户代表执行。
- 特点:测试用例通常由用户编写,关注用户的工作流程和业务规则。
-
性能测试(Performance Testing):
- 目的:评估软件应用的速度、响应时间、稳定性、资源消耗等性能指标。
- 范围:可以是单个组件也可以是整个系统。
- 特点:关注在高负载或特定条件下系统的行为。
-
回归测试(Regression Testing):
- 目的:确保软件变更没有引入新的错误。
- 范围:可以是单个单元、多个组件或整个系统。
- 特点:通常在代码修改或添加新功能后进行。
-
单元测试的目的
- 目的 :
- 早期发现错误:在开发周期的早期阶段捕捉和修复缺陷。
- 提供文档:测试用例可以作为代码行为的文档。
- 支持重构:确保在修改代码后原有功能仍然正常工作。
单元测试的基本原则
-
关键特点 :
-
独立性:
- 每个单元测试应该独立于其他测试运行,不依赖于系统的其他部分或外部环境的状态。
- 测试不应依赖于数据库、文件系统、网络或任何全局状态。
-
原子性 :
- 每个单元测试应该只测试一个具体的功能点或逻辑分支。
- 避免在一个测试中验证多个功能,以确保测试的明确性和易于理解。
-
可重复性 :
- 无论何时何地执行,测试都应该产生相同的结果。
- 这意味着测试不应该依赖于系统状态或时间敏感的操作。
-
测试框架的选择和介绍
-
JUnit (Java)
- 特点:JUnit是Java语言中最流行的单元测试框架之一,广泛用于Java和Android应用开发。JUnit 4主要使用注解来标识测试方法,而JUnit 5(Jupiter)则引入了更多的功能和改进。
- 适用场景:适用于Java和Kotlin项目,特别是需要大量自动化测试的企业级应用。
-
TestNG
- 特点:TestNG是JUnit的一个增强版,提供了更多的功能,如参数化测试、并行测试执行等。它同样使用注解来定义测试方法。
- 适用场景:适用于需要复杂测试配置和并行测试的大型Java应用。
-
pytest (Python)
- 特点:pytest是一个简单而强大的Python测试框架,支持简单的断言和参数化测试,并且可以很容易地与持续集成系统结合。
- 适用场景:适用于Python项目,特别是科学计算和数据分析领域。
-
NUnit (.NET)
- 特点:NUnit是.NET平台上的一个单元测试框架,受到JUnit的启发。它支持属性、断言和测试运行程序。
- 适用场景:适用于C#、F#和VB.NET项目,特别是需要集成.NET特定功能和特性的应用程序。
-
Mocha (JavaScript/Node.js)
- 特点:Mocha是一个灵活的JavaScript测试框架,适用于Node.js和浏览器。它支持异步测试,并且可以与其他断言库(如Chai)结合使用。
- 适用场景:适用于JavaScript项目,特别是在需要处理异步操作和Promise的场景。
-
RSpec (Ruby)
- 特点:RSpec是一个用于Ruby语言的行为驱动开发(BDD)框架,提供了一种表达性强的语法来编写测试。
- 适用场景:适用于Ruby on Rails项目,特别是那些采用BDD方法论的团队。
-
Google Test (C++)
- 特点:Google Test是一个用于C++的测试框架,提供了丰富的断言和测试组织功能。
- 适用场景:适用于C++项目,特别是需要高性能和复杂测试结构的应用程序。
-
Karma (JavaScript)
- 特点:Karma是一个测试运行器,可以为JavaScript应用提供实时的测试反馈。它通常与Mocha和Chai等断言库一起使用。
- 适用场景:适用于需要在不同浏览器上进行测试的JavaScript前端应用。
-
Selenium
- 特点:虽然Selenium主要用于自动化Web浏览器交互,但它也可以用来执行Web应用的单元测试。
- 适用场景:适用于Web应用的自动化测试,特别是需要模拟用户交互的场景。
-
选择测试框架时的考虑因素:
- 语言和平台支持:选择与你的编程语言和开发平台兼容的框架。
- 社区和文档:选择有良好文档和活跃社区支持的框架,以便在遇到问题时能够快速找到解决方案。
- 功能需求:根据项目需求选择提供所需功能的框架,例如参数化测试、并行测试等。
- 集成需求:考虑框架与现有开发工具和持续集成系统的集成能力。
- 团队熟悉度:选择团队成员熟悉或愿意学习的框架,以减少学习曲线。
编写测试用例
-
正向测试(Positive Testing)
-
定义:验证程序在正常或预期条件下的行为。
-
方法 :
- 确定代码的正常使用场景。
- 设计测试用例以验证主要功能和流程。
- 确保测试覆盖了最常见的使用情况。
-
边界条件测试(Boundary Value Analysis)
-
定义:验证程序在边界或极端条件下的行为。
-
方法 :
- 确定输入或循环的边界值,如数组的最小长度、最大长度、空数组等。
- 测试循环的开始和结束条件。
- 检查输入值的上限和下限。
-
等价类划分(Equivalence Partitioning)
-
定义:将输入数据划分为若干等价类,每个类的行为预期是相同的。
-
方法 :
- 识别有效和无效的输入值集合。
- 从每个等价类中选择至少一个测试用例。
-
错误猜测(Error Guessing)
-
定义:基于经验和直觉,猜测可能存在缺陷的代码区域。
-
方法 :
- 识别代码中可能出错的逻辑。
- 设计测试用例来验证这些猜测。
-
测试用例的结构
-
标题:明确测试用例的目的。
-
前提条件:测试执行前必须满足的条件。
-
测试步骤:详细描述执行测试的每个步骤。
-
预期结果:定义测试执行后的预期行为或输出。
-
实际结果:记录测试执行后的实际行为或输出。
-
测试状态:标记测试通过或失败的状态。
-
断言的使用
-
断言的作用:
-
验证预期结果: 断言允许测试者明确地指出代码执行后应该产生的结果。
-
提供明确的错误信息: 当测试失败时,断言可以提供关于期望值和实际值差异的具体信息,这有助于快速定位问题。
-
简化测试逻辑: 通过使用断言,测试代码可以更加简洁和直观,因为断言通常集成在测试框架中。
-
自动化测试验证: 断言使得测试结果的验证自动化,无需手动检查输出。
-
提高测试覆盖率: 断言有助于确保测试覆盖了所有重要的执行路径和边界条件。
-
-
相等性断言:
- 验证两个值是否相等。
assertEquals(expectedValue, actualValue);
-
不等性断言:
- 验证两个值是否不相等。
assertNotEquals(notExpectedValue, actualValue);
-
真值断言:
- 验证一个条件是否为真。
assertTrue(condition);
-
假值断言:
- 验证一个条件是否为假。
assertFalse(condition);
-
异常断言:
- 验证代码执行时是否抛出了特定的异常。
assertThrows(ExpectedException.class, () -> { // 调用可能抛出异常的方法 });
-
范围断言:
- 验证一个值是否在特定的范围内。
assertGreater(expectedMin, actualValue); assertLess(expectedMax, actualValue);
-
正则表达式断言:
- 验证一个字符串是否匹配特定的正则表达式。
assertMatchesRegex("expectedPattern", actualString);
-
同一度断言:
- 验证两个引用是否指向同一个对象。
assertSame(expectedObject, actualObject);
测试的组织和管理
-
测试代码的组织结构
-
按功能组织:
- 测试代码通常按照被测试的功能模块组织,每个模块或类有自己的测试类。
-
目录结构:
- 在项目中创建一个专门的测试目录,如
test
或tests
,在这个目录下进一步按模块划分子目录。
- 在项目中创建一个专门的测试目录,如
-
测试类和方法:
- 测试类通常以被测试类的名字命名,后跟
Test
作为后缀。 - 测试方法的命名应清晰表达测试的意图,如
testAdditionPositiveNumbers
。
- 测试类通常以被测试类的名字命名,后跟
-
使用命名空间(针对某些语言):
- 在支持命名空间的语言中,使用命名空间来组织测试代码,避免命名冲突。
-
模块化测试代码:
- 将测试代码分解为模块或包,每个模块包含相关的测试类和辅助类。
-
命名约定:
-
测试类命名:
- 遵循
ClassNameTest
或TestClassName
的命名模式。
- 遵循
-
测试方法命名:
- 使用
test_
前缀,后跟测试的场景或行为描述,如test_addition_with_negative_numbers
。
- 使用
-
常量和变量:
- 测试中使用的常量和变量应有明确和一致的命名规则。
-
避免使用缩写:
- 在命名测试用例和变量时,为了提高可读性,避免使用缩写。
-
测试的管理和执行策略:
-
自动化测试执行:
- 使用持续集成(CI)工具自动执行测试,确保代码提交后立即验证。
-
测试依赖管理:
- 确保测试代码不依赖于特定的运行顺序,每个测试都能独立运行。
-
测试数据管理:
- 使用工厂模式或测试数据构建器模式来管理测试数据,确保数据的一致性和可复用性。
-
测试环境隔离:
- 为测试提供一个隔离的环境,确保测试不会受到外部因素的干扰。
-
测试覆盖率目标:
- 设定代码覆盖率目标,使用工具持续监控测试覆盖率。
-
测试分层:
- 根据测试的范围和目的,将测试分层,如单元测试、集成测试、系统测试等。
-
测试报告:
- 生成详细的测试报告,包括通过率、失败的测试用例、测试覆盖率等信息。
-
测试维护:
- 定期审查和更新测试用例,确保它们与代码的当前状态保持一致。
-
测试代码审查:
- 将测试代码纳入代码审查过程,确保测试的质量。
-
测试优先级:
- 根据风险和重要性为测试用例设置优先级,优先执行关键功能的测试。
-
测试版本控制:
- 将测试代码纳入版本控制系统,与应用代码同步演进。
Mocking和Stubs
-
Mock对象:
- Mock对象是一个模拟的、假的实现对象,用于在单元测试中代替实际的依赖对象。
- 它主要用于验证被测试对象的行为,而不是依赖对象的行为。
-
Stubs:
- Stubs(存根)是提供预定响应的简单对象,通常用于模拟函数或方法的返回值。
- 它们用于设置测试环境,以便在测试中模拟外部依赖的特定行为。
-
应用场景:
-
当单元测试需要隔离外部依赖时,使用Mock对象和Stubs来模拟这些依赖的行为。
-
在测试中验证对象之间的交互,而不是它们的内部逻辑。
-
使用Mocking工具隔离外部依赖:
假设有一个
UserService
类,它依赖于一个UserRepository
来获取用户数据。在单元测试中,我们不想与数据库交互,而是想模拟UserRepository
的行为:@Test public void testGetUser() { // 创建Mock对象 UserRepository mockRepository = Mockito.mock(UserRepository.class); // 设置Mock行为 Mockito.when(mockRepository.findById(1)).thenReturn(new User(1, "TestUser")); // 创建被测试对象,并注入Mock的依赖 UserService userService = new UserService(mockRepository); // 调用方法并验证结果 User user = userService.getUser(1); assertEquals("TestUser", user.getName()); // 验证交互 Mockito.verify(mockRepository).findById(1); }
-
展示如何使用Mocking工具来隔离外部依赖。
测试覆盖率
-
重要性:
-
衡量测试的完整性:
- 测试覆盖率提供了一个量化的指标,用于衡量测试用例覆盖代码的程度。
-
发现未测试的代码:
- 高覆盖率可以减少未被测试的代码部分,从而降低引入缺陷的风险。
-
提高代码质量:
- 通过关注未被测试覆盖的代码区域,开发者可以提高代码的质量和健壮性。
-
使用工具衡量和提高测试覆盖率:
-
集成覆盖率工具:
- 在构建过程或持续集成流程中集成覆盖率工具,如JaCoCo、Cobertura或Istanbul。
-
分析覆盖率报告:
- 运行测试后,生成覆盖率报告,分析哪些代码区域没有被测试覆盖。
-
改进测试用例:
- 根据覆盖率报告,添加或改进测试用例,以覆盖未测试的代码。
-
设置覆盖率目标:
- 为项目设定合理的覆盖率目标,并持续跟踪达成情况。
-
持续改进:
- 将覆盖率作为代码审查和质量控制的一部分,持续改进测试覆盖率。
实验
一 实验目的:
1、了解什么是单元测试,单元测试的级别、单元测试的内容。
2、掌握单元测试框架JUnit的使用。
3、掌握参数化测试方法的运用及测试脚本的编写。
二 实验环境
1、JDK8.0或以上;
2、Intellij IDEA集成开发环境;
3、Maven构建工具。
三 实验准备
1、掌握JUnit测试框架的基本使用;
2、具备Java编程基础;
3、安装及配置好测试环境。
四 实验内容
(一)网上蛋糕购物系统中,针对蛋糕商品查询业务的持久类GoodsDao中的getGoodsById、getCountOfGoodsByTypeID方法编写单元测试类(一般情况下,单元测试要对每个方法进行测试)。
(1)创建GoodsDaoTest测试类,编写对应的测试方法。请提供GoodsDaoTest测试类代码,要求代码中要对每个方法进行注释说明。
java
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class GoodsDaoTest {
@Mock
private GoodsDataAccess goodsDataAccess; // 假设这是访问数据库的接口
@InjectMocks
private GoodsDao goodsDao; // 被测试的持久类
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this); // 初始化Mock对象
}
// 测试getGoodsById方法
@Test
public void testGetGoodsById_WithValidId_ShouldReturnCorrectGoods() {
// 准备
int validId = 1;
Goods expectedGoods = new Goods(validId, "Chocolate Cake", 20.0);
Mockito.when(goodsDataAccess.getGoodsById(validId)).thenReturn(expectedGoods);
// 执行
Goods result = goodsDao.getGoodsById(validId);
// 验证
assertNotNull(result, "返回的对象不应为空");
assertEquals(expectedGoods, result, "返回的蛋糕应与预期相符");
}
@Test
public void testGetGoodsById_WithInvalidId_ShouldReturnNull() {
// 准备
int invalidId = -1;
Mockito.when(goodsDataAccess.getGoodsById(invalidId)).thenReturn(null);
// 执行
Goods result = goodsDao.getGoodsById(invalidId);
// 验证
assertNull(result, "使用无效ID查询时,应返回null");
}
// 测试getCountOfGoodsByTypeID方法
@Test
public void testGetCountOfGoodsByTypeID_WithValidTypeId_ShouldReturnCorrectCount() {
// 准备
int validTypeId = 1;
int expectedCount = 10;
Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(validTypeId)).thenReturn(expectedCount);
// 执行
int result = goodsDao.getCountOfGoodsByTypeID(validTypeId);
// 验证
assertEquals(expectedCount, result, "返回的数量应与预期相符");
}
@Test
public void testGetCountOfGoodsByTypeID_WithInvalidTypeId_ShouldReturnZero() {
// 准备
int invalidTypeId = -1;
Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(invalidTypeId)).thenReturn(0);
// 执行
int result = goodsDao.getCountOfGoodsByTypeID(invalidTypeId);
// 验证
assertEquals(0, result, "使用无效类型ID查询时,应返回0");
}
}
代码解释:
@Mock
注解用于创建模拟对象。@InjectMocks
注解用于创建被测试类的实例,并将模拟对象注入到它的依赖中。@BeforeEach
注解的方法在每个测试方法执行之前都会运行,用于设置测试环境。@Test
注解的方法是实际的测试用例。Mockito.when(...).thenReturn(...)
用于定义模拟对象的行为。assertNotNull
、assertEquals
和assertNull
是JUnit提供的断言方法,用于验证测试结果是否符合预期。
(2)请设计3条测试用例测试getCountOfGoodsByTypeID方法,执行测试。
测试用例:
|----|--------|--------|------|------|------|
| 序号 | 测试用例编号 | 测试用例名称 | 输入数据 | 预期结果 | 测试结果 |
| | | | | | |
| | | | | | |
| | | | | | |
测试用例1: 有效商品类型ID
序号 | 测试用例编号 | 测试用例名称 | 输入数据 | 预期结果 | 测试结果 |
---|---|---|---|---|---|
1 | TC001 | 正常商品类型查询 | 1 | >0 | [待测试] |
说明:此测试用例验证当提供有效的商品类型ID时,方法应返回该类型下商品的正数数量。
测试用例2: 无效商品类型ID(负数)
序号 | 测试用例编号 | 测试用例名称 | 输入数据 | 预期结果 | 测试结果 |
---|---|---|---|---|---|
2 | TC002 | 负数商品类型查询 | -1 | 0 | [待测试] |
说明:此测试用例验证当商品类型ID为负数时,方法应返回0,表示没有商品匹配。
测试用例3: 边界商品类型ID(例如最大的正整数)
序号 | 测试用例编号 | 测试用例名称 | 输入数据 | 预期结果 | 测试结果 |
---|---|---|---|---|---|
3 | TC003 | 最大正整数商品类型查询 | Integer.MAX_VALUE | 0或实际数量 | [待测试] |
说明:此测试用例验证当商品类型ID为整数最大值时,方法的表现,可能是返回0或者实际的商品数量,取决于数据库中的数据。
(3)针对以上测试方法的不足(多条测试用例需要多次执行),根据参数化测试的规则,使用以上测试用例数据,修改测试方法并执行测试。请提供修改后的测试方法代码
java
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class GoodsDaoTest {
@Mock
private GoodsDataAccess goodsDataAccess;
@InjectMocks
private GoodsDao goodsDao;
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this);
}
// 参数化测试用例
@ParameterizedTest
@CsvSource({
"1, 10, 应返回正数的商品数量",
"-1, 0, 应返回0表示没有商品",
"2147483647, 5, 应返回实际的商品数量或0"
})
public void testGetCountOfGoodsByTypeID(int typeId, int expectedResult, String caseDescription) {
// 准备
Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(typeId)).thenReturn(expectedResult);
// 执行
int result = goodsDao.getCountOfGoodsByTypeID(typeId);
// 验证
assertEquals(expectedResult, result, caseDescription);
}
}
- 新建一个Foo类,使用Mockito框架对该类进行测试
public class Foo {
private Bar bar;
public void setBar(Bar bar) {
this.bar = bar;
}
public String doSomething() {
return "Foo::doSomething " + bar.doSomethingElse();
}
}
public class Bar {
public String doSomethingElse() {
return "Bar::doSomethingElse";
}
}
测试脚本截图为:
java
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
public class FooTest {
@Mock
private Bar mockBar; // 创建Bar的Mock对象
@InjectMocks
private Foo foo; // 被测试的Foo对象
@BeforeEach
public void setUp() {
// 初始化Mock对象
MockitoAnnotations.openMocks(this);
}
@Test
public void testDoSomething() {
// 准备
when(mockBar.doSomethingElse()).thenReturn("Bar::doSomethingElse");
// 执行
String result = foo.doSomething();
// 验证
verify(mockBar).doSomethingElse(); // 验证Bar的doSomethingElse方法被调用
assertEquals("Foo::doSomething Bar::doSomethingElse", result);
}
}
五 实验总结
(1)什么叫桩程序?桩程序有什么作用?
桩程序(Stub): 桩程序是一种模拟对象,用于在单元测试中代替实际的依赖项或外部系统。它通常用于模拟那些在测试环境中不可用或不适合使用的组件,如数据库、网络服务或复杂计算。
作用:
- 隔离测试:桩程序允许开发者在不依赖外部系统或复杂逻辑的情况下测试代码。
- 控制测试环境:通过返回预定的响应,桩程序可以控制测试环境,确保测试的一致性和可重复性。
- 提高测试速度:使用桩程序可以避免耗时的外部调用,从而加快测试执行速度。
- 简化测试逻辑:桩程序可以简化测试逻辑,使测试用例更专注于验证被测试代码的行为。
- 模拟错误情况:桩程序还可以模拟错误情况或异常响应,帮助测试代码的健壮性和错误处理能力。
(2)使用参数化测试有什么好处?
参数化测试是一种测试方法,它允许使用不同的输入参数多次执行同一个测试方法。以下是使用参数化测试的一些好处:
- 减少重复代码:参数化测试可以减少编写和维护多个相似测试用例的代码量。
- 提高测试效率:通过一次性执行多个测试用例,参数化测试可以提高测试的效率和覆盖率。
- 简化测试数据管理:集中管理测试数据,便于更新和维护。
- 增强测试的灵活性:可以轻松地添加、修改或删除测试参数,以适应不同的测试需求。
- 提高测试的可读性:通过清晰的参数化表达,测试用例的意图和行为更容易被理解。
- 支持边界值分析:参数化测试可以方便地测试边界值和异常值,提高代码的边界条件覆盖率。
- 自动化测试执行:参数化测试通常与自动化测试框架结合使用,实现测试的自动化执行。
- 易于维护和扩展:随着软件需求的变更,参数化测试可以更容易地进行更新和扩展。
- 提供一致的测试结果:确保每个测试用例在相同的测试逻辑下运行,提供一致的测试结果。