 Java开发中必备能力单元测试
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()/@spySpy 是指使用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
