Java开发中必备能力单元测试
# Java开发中必备能力单元测试
# 1. 什么是单元测试
单元测试(Unit Testing)是软件开发过程中一种用来验证代码功能的小规模测试方法。它的目标是验证代码的各个“单元”(通常是指函数或方法)的正确性。单元测试可以确保每个代码单元独立运行并输出预期结果,从而帮助开发者在早期阶段发现和修复错误,提高代码质量和维护性。
通常我们在开发的时候如果不适用单元测试就会在main方法或者业务逻辑中直接测试。
- 通过编写大量的 main 方法针对每个内容做打印输出到控制台枯燥繁琐,不具备优雅性。
- 测试方法不能一起运行,结果需要程序员自己判断正确性。
- 统一且重复性工作应该交给工具去完成
本博客参考:https://zhuanlan.zhihu.com/p/608775174
# 2. 编写单元测试的基本步骤
- 选择测试框架:选择适合的单元测试框架,例如Python的unittest、pytest,Java的JUnit,JavaScript的Jest等。
- 编写测试用例:为每个代码单元编写测试用例,包括输入、预期输出和断言。
- 运行测试:运行单元测试框架,查看测试结果,确保所有测试用例通过。
- 修复错误:如果测试未通过,检查代码并修复错误,直到所有测试用例通过。
# 3. 单元测试的方法
# 3.1 白盒测试(White Box Testing)
在白盒测试中,测试人员了解代码的内部结构和实现细节,编写测试用例来覆盖不同的代码路径和逻辑条件。白盒测试主要关注以下方面:
- 代码覆盖率:确保每行代码都被执行过。
- 分支覆盖率:确保每个逻辑分支(如if-else语句)都被测试过。
- 路径覆盖率:确保所有可能的执行路径都被测试过。
# 3.2黑盒测试(Black Box Testing)
黑盒测试不考虑代码的内部实现,而是基于需求规格说明或功能规范编写测试用例,测试程序的输入和输出是否符合预期。黑盒测试主要关注以下方面:
- 功能测试:确保每个功能按照预期工作。
- 边界值测试:测试输入的边界条件(如最小值、最大值、临界值等)。
- 异常处理测试:确保程序在遇到异常情况时能够正确处理。
# 4.单元测试框架JUnit
# 4.1 JUnit 简介
JUnit 官网 (opens new window):JUnit 是一个用于编写可重复测试的简单框架,广泛用于 Java 语言的单元测试。它是 xUnit 单元测试框架系列中的一个代表,具有以下特点:
- 专为 Java 语言设计,使用广泛。
- 是标准的测试框架,涵盖特定领域。
- 支持多种 IDE 开发平台的集成,如 IntelliJ IDEA、Eclipse。
- 可以通过 Maven 轻松引入和使用。
- 方便编写单元测试代码并查看测试结果。
JUnit 的重要概念:
名称 | 功能作用 |
---|---|
Assert | 断言方法集合 |
TestCase | 表示一个测试案例 |
TestSuite | 包含一组 TestCase,构成一组测试 |
TestResult | 收集测试结果 |
JUnit 的注意事项及规范:
- 测试方法必须使用
@Test
注解。 - 测试方法必须为
public void
,且不能带参数。 - 测试代码包应与被测试代码包结构保持一致。
- 测试单元中的每个方法应独立测试,方法间不能有依赖。
- 测试类一般使用
Test
作为类名后缀。 - 测试方法通常以
test
作为方法名前缀。
JUnit 失败结果说明:
- Failure:测试结果与预期结果不一致导致的失败。
- Error:由异常引起,可能源自测试代码或被测试代码中的错误。
# 4.2 JUnit 内容
# 4.2.1 断言 API
JUnit 提供了多种断言方法用于验证测试结果:
断言方法 | 描述 |
---|---|
assertNull(String message, Object object) | 检查对象是否为空,不为空则报错 |
assertNotNull(String message, Object object) | 检查对象是否不为空,为空则报错 |
assertEquals(String message, Object expected, Object actual) | 检查两个对象值是否相等,不相等则报错 |
assertTrue(String message, boolean condition) | 检查条件是否为真,不为真则报错 |
assertFalse(String message, boolean condition) | 检查条件是否为假,为真则报错 |
assertSame(String message, Object expected, Object actual) | 检查对象引用是否相同,不同则报错 |
assertNotSame(String message, Object unexpected, Object actual) | 检查对象引用是否不同,相同则报错 |
assertArrayEquals(String message, Object[] expecteds, Object[] actuals) | 检查数组值是否相等,不相等则报错 |
assertThat(String reason, T actual, Matcher<? super T> matcher) | 检查对象是否满足给定规则,不满足则报错 |
# 4.2.2 JUnit 常用注解
@Test
:定义一个测试方法。可以使用expected
属性来指定测试期望抛出的异常类,或使用timeout
属性设定测试方法的执行时间限制。@BeforeClass
:在所有测试方法执行之前运行,通常用于全局设置,static
方法且只运行一次。@AfterClass
:在所有测试方法执行完毕后运行,通常用于全局资源清理,static
方法且只运行一次。@Before
:在每个测试方法执行前运行,用于初始化操作。@After
:在每个测试方法执行后运行,用于资源回收。@Ignore
:忽略指定的测试方法,不执行。@RunWith
:更改测试运行器。- @RunWith(JUnit4.class) 就是指用JUnit4来运行
- @RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境 ,此时需要搭配@ContextConfiguration 使用,Spring整合JUnit4测试时,使用注解引入多个配置文件
- @RunWith(Suite.class) 的话就是一套测试集合
# 4.3 JUnit 使用
在 Spring Boot 项目中,可以使用 Maven 依赖引入 JUnit:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2
3
4
5
# 4.3 单元测试
在开发过程中,单元测试是保障代码质量的重要手段。通过对各个模块进行单独测试,可以有效地发现和修复代码中的问题。以下我们将分别展示对 Controller 层、Service 层、Dao 层的单元测试示例,并解释其中的关键点。
# 4.3.1 Controller 层单元测试
Controller 层负责处理用户的 HTTP 请求并返回相应的结果。通过使用 Spring 提供的 MockMvc
工具,可以在不启动整个应用程序的情况下,对 Controller 层进行单元测试。
下面是一个简单的 Controller 层单元测试示例:
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MainApplication.class)
public class StudentControllerTest {
@Autowired
private WebApplicationContext applicationContext;
private MockMvc mockMvc;
@Before
public void setupMockMvc() {
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
}
@Test
public void addStudent() throws Exception {
String json = "{\"name\":\"张三\",\"className\":\"三年级一班\",\"age\":20,\"sex\":\"男\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/student/save")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
.andDo(MockMvcResultHandlers.print());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
解释:
- MockMvc 是 Spring 提供的工具,用于在测试环境中模拟 HTTP 请求和响应。在这里,
MockMvc
被配置为使用应用程序上下文(WebApplicationContext
)来初始化,以便模拟请求的环境与实际运行时环境一致。 @Before
注解 用于在每个测试方法执行之前,初始化MockMvc
实例。@Test
注解 标记了一个测试方法。在addStudent
测试方法中,我们构建了一个 POST 请求,发送包含学生信息的 JSON 数据,并验证返回的 HTTP 状态码是否为 200(即isOk()
)。同时,我们还通过jsonPath
验证返回的 JSON 数据中是否包含期望的字段和值。
# 4.3.2 Service 层单元测试
Service 层主要负责业务逻辑处理。对 Service 层的单元测试可以确保业务逻辑的正确性。
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@Test
public void getOne() {
Student stu = studentService.selectByKey(5);
Assert.assertNotNull("学生信息不应为空", stu);
Assert.assertThat(stu.getName(), CoreMatchers.is("张三"));
}
@Test
public void addStudent() {
Student newStudent = new Student();
newStudent.setName("李四");
newStudent.setClassName("三年级二班");
newStudent.setAge(21);
newStudent.setSex("男");
boolean isAdded = studentService.save(newStudent);
Assert.assertTrue("学生添加失败", isAdded);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
解释:
- Service 层的单元测试 主要是测试业务逻辑的完整性。在这个例子中,我们测试了查询和添加学生的功能,确保方法返回的结果符合预期。
Assert.assertNotNull
用于验证返回的学生对象不为空,Assert.assertThat
用于检查对象的某个属性是否符合预期。@Test
注解 的每个方法都是一个独立的测试单元,可以单独执行以验证不同的业务逻辑。
# 4.3.3 Dao 层单元测试
Dao 层负责与数据库的交互,通过单元测试可以验证数据访问层的 CRUD 操作是否正常。
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentDaoTest {
@Autowired
private StudentMapper studentMapper;
@Test
@Rollback
@Transactional
public void insertOne() {
Student student = new Student();
student.setName("李四");
student.setMajor("计算机学院");
student.setAge(25);
student.setSex("男");
int count = studentMapper.insert(student);
Assert.assertEquals("插入操作应成功,返回影响行数应为1", 1, count);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
解释:
@Rollback
和@Transactional
注解 用于确保测试过程中对数据库的修改不会持久化。测试结束后,数据库会回滚到测试前的状态,避免污染数据。Assert.assertEquals
用于检查插入操作的影响行数是否与预期相符(即成功插入一条记录)。
# 4.3.4 异常测试
在 Service 层中,某些方法可能会抛出异常,单元测试可以验证这些异常是否按预期抛出。
public void computeScore() {
int a = 10, b = 0;
int result = a / b; // 抛出 ArithmeticException
}
@Test(expected = ArithmeticException.class)
public void computeScoreTest() {
studentService.computeScore();
}
2
3
4
5
6
7
8
9
解释:
- 异常测试 通过
@Test(expected = ...)
注解,可以验证某个方法在特定情况下是否会抛出预期的异常。 - 在这个例子中,
computeScore
方法尝试进行除以零的操作,从而抛出ArithmeticException
。测试方法computeScoreTest
通过指定expected
参数,期望该异常被抛出。这个注解告诉 JUnit,在运行这个测试方法时,预期会抛出指定类型的异常。如果异常被抛出,测试通过;如果没有抛出异常,测试失败。
# 4.3.5 测试套件
JUnit 支持将多个测试类组合在一起运行,这可以帮助我们在一次运行中执行多个单元测试。
@RunWith(Suite.class)
@Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class })
public class AllTests {
}
2
3
4
解释:
- 测试套件 通过
@Suite.SuiteClasses
注解,我们可以指定要运行的测试类列表。AllTests
类将StudentServiceTest
和StudentDaoTest
组合在一起,一次性运行这两个测试类中的所有测试方法。
好的,我将对这段内容进行简化和优化,以便更清晰地传达 Mockito 的核心概念和使用方法。
# 5 单元测试工具 - Mockito
# 5.1 Mockito 简介
在单元测试中,我们常常需要模拟外部依赖(如数据库、接口)来隔离待测试的代码。Mockito 是一个流行的 Java 模拟框架,可以帮助我们创建虚拟对象(Mock 对象)以便在测试中模拟这些外部依赖,。其官网为 Mockito (opens new window)。
Mockito 的主要应用场景包括:
- 难以构造的对象:某些对象的创建依赖复杂的外部环境或大量资源,例如要调用其他服务。
- 难以触发的行为:某些行为在测试中难以模拟,例如特定的网络响应。
- 未开发的依赖:依赖的接口或功能尚未实现。
Mockito 的主要特点:
- 支持模拟类和接口。
- 通过注解简化使用。
- 支持方法调用的顺序验证。
- 提供参数匹配器,灵活处理参数。
# 5.2 Mockito 使用
在 Spring 项目中,引入 spring-boot-starter-test
依赖即可自动引入 Mockito。
# 5.2.1 使用示例
定义一个类
创建一个BookService
类,并创建2个方法:public class BookService { public Book orderBook(String name){ return new Book(name); } public String hello(){ return "hello"; } }
1
2
3
4
5
6
7
8编写单元测试
使用 Mockito 的mock静态方法模拟BookService
的行为并测试StudentService
的orderBook
方法:class BookServiceTest { @Test void orderBook() { BookService bookService = Mockito.mock(BookService.class); Book expectedBook = new Book("钢铁是怎样炼成的"); Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook); Book actualBook = bookService.orderBook(""); // 验证逻辑 assertEquals(expectedBook, actualBook); } }
1
2
3
4
5
6
7
8
9
10
11
解析:
尽管 BookService
的 orderBook
方法没有实现,但通过 Mockito 的模拟,我们可以指定当 orderBook
被调用时返回的结果。这使得单元测试能够独立于实际的依赖进行验证。
# 5.2.2 常用 API
mock()/@Mock
:Mock是指使用Mockito创建的模拟对象,它模拟真实对象的行为,用于替代真实对象的依赖项,以便进行独立的单元测试spy()/@spy
Spy 是指使用Mockito创建的部分模拟对象,它保留了真实对象的部分行为。Spy对象既可以模拟方法的返回值,也可以保留方法的实际行为。@InjectMocks
: @InjectMocks是一个Mockito注解,用于自动将模拟对象注入到被测对象中的相应字段中。when().thenReturn()
:定义当某方法被调用时返回指定结果。any(Class)
:匹配任何类型的参数。verify()
:验证方法是否被调用。doThrow()
:定义方法调用时抛出异常。
@InjectMocks是一个Mockito注解,用于自动将模拟对象注入到被测对象中的相应字段中。
# 5.2.3 使用要点
打桩(Stubbing)
预定义某个方法的行为:Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook);
1参数匹配
使用参数匹配器灵活处理输入:Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook);
1次数验证
验证方法调用次数:verify(mockedList, times(3)).get(1);
1异常测试
验证方法是否抛出指定异常:@Test(expected = RuntimeException.class) public void exceptionTest() { List mockedList = mock(List.class); doThrow(new RuntimeException()).when(mockedList).add(1); mockedList.add(1); // 验证通过 }
1
2
3
4
5
6
# 5.24 注解方式使用注意
使用注解方式,可以通过注解来自动创建和注入模拟对象。 使用@Mock注解在测试类中声明模拟对象的字段,然后使用@InjectMocks注解将模拟对象自动注入到被测对象中,注解方式更加简洁和便捷,省去了手动创建和配置模拟对象的步骤。 * 需要注意,通过注解的方式,需要在 类上加入@ExtendWith(MockitoExtension.class)注解。
public class StudentService {
private BookService bookService;
public Book studentBook(){
return bookService.orderBook("我的书");
}
}
@ExtendWith(MockitoExtension.class) // 自动初始化 @Mock 和 @InjectMocks 注解
class BookServiceTest {
@Mock
BookService bookService;
@InjectMocks
private StudentService studentService;//将bookService注入到studentService对象中,有没有构造方法都可以好像
@Test
void orderBook() {
Book expectedBook = new Book("钢铁是怎样炼成的");
Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook);
Book actualBook = studentService.studentBook();
// 验证逻辑
assertEquals(expectedBook, actualBook); //true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25