Yang's blog Yang's blog
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xiaoyang

编程爱好者
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • SpringCloud

    • 微服务架构介绍
    • SpringCloud介绍
    • Spring Cloud:生产者与消费者
    • Spring Cloud Eureka:构建可靠的服务注册与发现
    • Spring Cloud Ribbon:负载均衡
    • Spring Cloud Fegin:服务调用
    • Spring Cloud Hystrix:熔断器
    • Spring Cloud Zuul:统一网关路由
    • Spring Cloud Config:配置中心
  • Java后端框架

    • LangChain4j

      • 介绍
      • 快速开始
      • Chat and Language Models
      • Chat Memory
      • Model Parameters
      • Response Streaming
      • AI Services
      • Agent
      • Tools (Function Calling)
      • RAG
      • Structured Outputs
      • Classification
      • Embedding (Vector) Stores
      • Image Models
      • Quarkus Integration
      • Spring Boot Integration
      • Kotlin Support
      • Logging
      • Observability
      • Testing and Evaluation
      • Model Context Protocol
  • 八股文

    • 操作系统
    • JVM介绍
    • Java多线程
    • Java集合框架
    • Java反射
    • JavaIO
    • Mybatis介绍
    • Spring介绍
    • SpringBoot介绍
    • Mysql
    • Redis
    • 数据结构
    • 云计算
    • 设计模式
    • 计算机网络
    • 锁核心类AQS
    • Nginx
  • 前端技术

    • 初识Vue3
    • Vue3数据双向绑定
    • Vue3生命周期
    • Vue-Router 组件
    • Pinia 集中式状态存储
  • 中间件

    • RocketMQ
  • 开发知识

    • 请求参数注解
    • 时间复杂度和空间复杂度
    • JSON序列化与反序列化
    • Timestamp vs Datetime
    • Java开发中必备能力单元测试
      • 1. 什么是单元测试
      • 2. 编写单元测试的基本步骤
      • 3. 单元测试的方法
        • 3.1 白盒测试(White Box Testing)
        • 3.2黑盒测试(Black Box Testing)
      • 4.单元测试框架JUnit
        • 4.1 JUnit 简介
        • 4.2 JUnit 内容
        • 4.2.1 断言 API
        • 4.2.2 JUnit 常用注解
        • 4.3 JUnit 使用
        • 4.3 单元测试
        • 4.3.1 Controller 层单元测试
        • 4.3.2 Service 层单元测试
        • 4.3.3 Dao 层单元测试
        • 4.3.4 异常测试
        • 4.3.5 测试套件
      • 5 单元测试工具 - Mockito
        • 5.1 Mockito 简介
        • 5.2 Mockito 使用
        • 5.2.1 使用示例
        • 5.2.2 常用 API
        • 5.2.3 使用要点
        • 5.24 注解方式使用注意
    • 正向代理和反向代理
    • 什么是VPN
    • 正则表达式
  • Java
  • 开发知识
xiaoyang
2024-07-29
目录

Java开发中必备能力单元测试

# Java开发中必备能力单元测试

# 1. 什么是单元测试

单元测试(Unit Testing)是软件开发过程中一种用来验证代码功能的小规模测试方法。它的目标是验证代码的各个“单元”(通常是指函数或方法)的正确性。单元测试可以确保每个代码单元独立运行并输出预期结果,从而帮助开发者在早期阶段发现和修复错误,提高代码质量和维护性。

通常我们在开发的时候如果不适用单元测试就会在main方法或者业务逻辑中直接测试。

  1. 通过编写大量的 main 方法针对每个内容做打印输出到控制台枯燥繁琐,不具备优雅性。
  2. 测试方法不能一起运行,结果需要程序员自己判断正确性。
  3. 统一且重复性工作应该交给工具去完成

本博客参考:https://zhuanlan.zhihu.com/p/608775174

# 2. 编写单元测试的基本步骤

  1. 选择测试框架:选择适合的单元测试框架,例如Python的unittest、pytest,Java的JUnit,JavaScript的Jest等。
  2. 编写测试用例:为每个代码单元编写测试用例,包括输入、预期输出和断言。
  3. 运行测试:运行单元测试框架,查看测试结果,确保所有测试用例通过。
  4. 修复错误:如果测试未通过,检查代码并修复错误,直到所有测试用例通过。

# 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 单元测试框架系列中的一个代表,具有以下特点:

  1. 专为 Java 语言设计,使用广泛。
  2. 是标准的测试框架,涵盖特定领域。
  3. 支持多种 IDE 开发平台的集成,如 IntelliJ IDEA、Eclipse。
  4. 可以通过 Maven 轻松引入和使用。
  5. 方便编写单元测试代码并查看测试结果。

JUnit 的重要概念:

名称 功能作用
Assert 断言方法集合
TestCase 表示一个测试案例
TestSuite 包含一组 TestCase,构成一组测试
TestResult 收集测试结果

JUnit 的注意事项及规范:

  1. 测试方法必须使用 @Test 注解。
  2. 测试方法必须为 public void,且不能带参数。
  3. 测试代码包应与被测试代码包结构保持一致。
  4. 测试单元中的每个方法应独立测试,方法间不能有依赖。
  5. 测试类一般使用 Test 作为类名后缀。
  6. 测试方法通常以 test 作为方法名前缀。

JUnit 失败结果说明:

  1. Failure:测试结果与预期结果不一致导致的失败。
  2. 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 常用注解

  1. @Test:定义一个测试方法。可以使用 expected 属性来指定测试期望抛出的异常类,或使用 timeout 属性设定测试方法的执行时间限制。
  2. @BeforeClass:在所有测试方法执行之前运行,通常用于全局设置,static 方法且只运行一次。
  3. @AfterClass:在所有测试方法执行完毕后运行,通常用于全局资源清理,static 方法且只运行一次。
  4. @Before:在每个测试方法执行前运行,用于初始化操作。
  5. @After:在每个测试方法执行后运行,用于资源回收。
  6. @Ignore:忽略指定的测试方法,不执行。
  7. @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>
1
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());
    }
}
1
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);
    }
}
1
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);
    }
}
1
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();
}
1
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 {
}
1
2
3
4

解释:

  • 测试套件 通过 @Suite.SuiteClasses 注解,我们可以指定要运行的测试类列表。AllTests 类将 StudentServiceTest 和 StudentDaoTest 组合在一起,一次性运行这两个测试类中的所有测试方法。

好的,我将对这段内容进行简化和优化,以便更清晰地传达 Mockito 的核心概念和使用方法。


# 5 单元测试工具 - Mockito

# 5.1 Mockito 简介

在单元测试中,我们常常需要模拟外部依赖(如数据库、接口)来隔离待测试的代码。Mockito 是一个流行的 Java 模拟框架,可以帮助我们创建虚拟对象(Mock 对象)以便在测试中模拟这些外部依赖,。其官网为 Mockito (opens new window)。

Mockito 的主要应用场景包括:

  • 难以构造的对象:某些对象的创建依赖复杂的外部环境或大量资源,例如要调用其他服务。
  • 难以触发的行为:某些行为在测试中难以模拟,例如特定的网络响应。
  • 未开发的依赖:依赖的接口或功能尚未实现。

Mockito 的主要特点:

  1. 支持模拟类和接口。
  2. 通过注解简化使用。
  3. 支持方法调用的顺序验证。
  4. 提供参数匹配器,灵活处理参数。

# 5.2 Mockito 使用

在 Spring 项目中,引入 spring-boot-starter-test 依赖即可自动引入 Mockito。

# 5.2.1 使用示例

image-20240808205450643

  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
  2. 编写单元测试
    使用 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

  1. mock()/@Mock:Mock是指使用Mockito创建的模拟对象,它模拟真实对象的行为,用于替代真实对象的依赖项,以便进行独立的单元测试
  2. spy()/@spy Spy 是指使用Mockito创建的部分模拟对象,它保留了真实对象的部分行为。Spy对象既可以模拟方法的返回值,也可以保留方法的实际行为。
  3. @InjectMocks: @InjectMocks是一个Mockito注解,用于自动将模拟对象注入到被测对象中的相应字段中。
  4. when().thenReturn():定义当某方法被调用时返回指定结果。
  5. any(Class):匹配任何类型的参数。
  6. verify():验证方法是否被调用。
  7. doThrow():定义方法调用时抛出异常。

@InjectMocks是一个Mockito注解,用于自动将模拟对象注入到被测对象中的相应字段中。

# 5.2.3 使用要点

  1. 打桩(Stubbing)
    预定义某个方法的行为:

    Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook);
    
    1
  2. 参数匹配
    使用参数匹配器灵活处理输入:

    Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectedBook);
    
    1
  3. 次数验证
    验证方法调用次数:

    verify(mockedList, times(3)).get(1);
    
    1
  4. 异常测试
    验证方法是否抛出指定异常:

    @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
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
编辑 (opens new window)
上次更新: 2025/04/01, 01:48:12

← Timestamp vs Datetime 正向代理和反向代理→

最近更新
01
操作系统
03-18
02
Nginx
03-17
03
后端服务端主动推送消息的常见方式
03-11
更多文章>
Theme by Vdoing | Copyright © 2023-2025 xiaoyang | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式