Spring Boot 单元测试详细说明文档

聊聊单元测试(三)——Spring Test+JUnit完美组合 这两天做Web开发,发现通过Spring进行对象管理之后,做测试变得复杂了。因为所有的Bean都需要在applicationContext.xml中加载好,之后再通过@Resource去取得。如果每次都要整个业务流做的差不多了再去测试,这样效率很低,也很麻烦。如果单独去写一个测试用类,这样太繁琐了。于是想起Spring有一个测试框架,能够整合JUnit进行测试,于是便拿出来试试~ 阅读详情

Spring Boot 单元测试是针对 Spring Boot 应用中单个组件(如服务类、工具类、控制器等)的最小可测试单元,进行独立、快速、自动化验证的测试方法。它不依赖外部系统(如数据库、网络服务),通过模拟(Mock)依赖项来隔离被测代码,确保测试的稳定性和高效性。


单元测试的作用

  1. 验证逻辑正确性:确保每个方法在各种输入条件下都能返回预期结果。
  2. 快速反馈:在代码修改后立即发现回归缺陷,显著缩短调试周期。
  3. 提高代码质量:迫使开发者思考边界条件和异常路径,促进代码设计更清晰、可维护。
  4. 文档作用:良好的测试用例本身就是“可执行的文档”,说明了方法如何使用。
  5. 支持重构安全:在重构代码时,有完整测试覆盖可确保功能不变,增强信心。
  6. 提升团队协作效率:减少因代码变更导致的意外问题,降低集成阶段的冲突。

为什么需要写单元测试?

  • 降低维护成本:后期修复 Bug 的成本远高于预防。
  • 保障核心业务稳定:尤其在金融、保险等高可靠性领域,逻辑错误可能导致严重后果。
  • 支持持续集成(CI):自动化构建流程必须通过单元测试才能通过。
  • 提升开发自信:有测试覆盖的代码,开发者更敢改、敢优化。

单元测试的标准写法(JUnit 5 + Mockito + Spring Boot)

标准写法遵循 AAA 模式:

  • Arrange(准备):初始化被测对象、设置输入数据、模拟依赖。
  • Act(执行):调用被测方法。
  • Assert(断言):验证输出结果或行为是否符合预期。

关键原则:

  • 每个测试方法只测试一个场景
  • 测试名称清晰表达意图(如 shouldReturnUserWhenValidIdProvided)
  • 避免测试逻辑复杂,不涉及 I/O、网络、数据库
  • 使用 @Test 注解,不使用 @SpringBootTest(那是集成测试)

单元测试的目标

  • 验证被测单元的功能行为是否符合设计规范
  • 确保代码在各种边界条件和异常输入下表现正确
  • 建立可重复、自动化、快速执行的验证机制

单元测试的目标是功能正确性 + 可维护性 + 快速反馈,而非系统整体运行。


推荐对私有方法进行单元测试吗?

❌ 不推荐直接测试私有方法。

原因:

  • 私有方法是实现细节,不是对外接口。测试应关注行为而非实现。
  • 直接测试私有方法会破坏封装性,使测试与实现强耦合,重构时易失效。
  • 私有方法通常被公有方法调用,通过测试公有方法间接覆盖私有逻辑即可。

✅ 正确做法:
确保公有方法的测试用例覆盖了私有方法的所有执行路径(如分支、异常等)。


单元测试中的疑难杂点

问题说明解决方案
依赖外部服务(如数据库、HTTP)导致测试慢、不可靠使用 @MockBean 或 Mockito.mock() 模拟依赖
测试随机数、时间、UUID结果不稳定使用 Mockito.when(...).thenReturn(...) 固定返回值
测试异步方法无法立即获取结果使用 awaitility 或 CountDownLatch 等待完成
测试 Spring Bean 注入需要上下文启动太慢使用 @ExtendWith(MockitoExtension.class) + 手动 new 对象,避免 @SpringBootTest
测试异常抛出需验证异常类型和信息使用 assertThrows() 捕获并断言
测试私有方法或静态方法不易直接调用通过公有方法间接测试;静态方法可重构为工具类+接口抽象

单元测试常用的工具

工具用途
JUnit 5主流 Java 单元测试框架,支持 @Test, @BeforeEach, @Disabled 等
Mockito模拟对象(Mock),用于隔离依赖,如 when(service.get()).thenReturn(...)
AssertJ更流畅、可读性更强的断言库(如 assertThat(result).isEqualTo(...))
Hamcrest提供更灵活的匹配器(如 hasProperty("name", is("张三")))
Awaitility用于异步操作的等待断言
Spring Boot Test提供 @WebMvcTest, @DataJpaTest 等注解,用于切片测试(非全上下文)
JaCoCo代码覆盖率统计工具,用于衡量测试覆盖程度

单元测试中的重要概念

概念说明
测试隔离(Isolation)每个测试独立,不依赖其他测试状态
SUT(System Under Test)被测试的系统/对象
Mock / StubMock:模拟对象行为并验证调用;Stub:返回预设值,不验证调用
测试覆盖率衡量代码被测试执行的比例(行、分支、方法)
测试金字塔单元测试 > 集成测试 > 端到端测试,应以单元测试为主力
测试双倍(Test Double)包括 Mock、Stub、Fake、Dummy、Spy 等模拟对象类型
TDD(测试驱动开发)先写测试,再写实现,驱动设计

综合性单元测试示例(带详细中文注释)

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDate;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

// 使用 MockitoExtension 自动注入 @Mock 和 @InjectMocks,无需手动 initMocks
@ExtendWith(MockitoExtension.class)
class InsuranceClaimServiceTest {

    // 模拟依赖:保险理赔申请仓库(Repository)
    @Mock
    private InsuranceClaimRepository claimRepository;

    // 模拟依赖:客户信息服务(外部服务)
    @Mock
    private CustomerService customerService;

    // 被测试的系统(SUT):理赔服务类
    @InjectMocks
    private InsuranceClaimService claimService;

    // 测试数据准备(每个测试前执行)
    @BeforeEach
    void setUp() {
        // 初始化 Mockito 注解(如果未使用 @ExtendWith,则需手动调用 MockitoAnnotations.openMocks(this))
        MockitoAnnotations.openMocks(this);
    }

    /**
     * 测试场景:当用户提交合法的理赔申请时,系统应成功创建并返回理赔ID
     * 场景:客户存在、申请金额合理、无重复申请
     */
    @Test
    void shouldCreateClaimSuccessfullyWhenValidRequest() {
        // Arrange:准备测试数据
        Long customerId = 1001L;
        String claimDescription = "车祸理赔";
        Double claimAmount = 5000.0;
        String claimId = "CLAIM-2025-001";

        // 模拟客户信息服务:当调用 getCustomer(1001L) 时,返回一个有效客户对象
        when(customerService.getCustomer(customerId))
            .thenReturn(new Customer(customerId, "张三", "active"));

        // 模拟仓库:当调用 findByCustomerIdAndStatus(customerId, "PENDING") 时,返回空(无重复申请)
        when(claimRepository.findByCustomerIdAndStatus(customerId, "PENDING"))
            .thenReturn(Optional.empty());

        // 模拟仓库:当调用 save() 时,保存后返回一个包含生成ID的理赔对象
        InsuranceClaim savedClaim = new InsuranceClaim(claimId, customerId, claimDescription, claimAmount, "PENDING", LocalDate.now());
        when(claimRepository.save(any(InsuranceClaim.class))).thenReturn(savedClaim);

        // Act:执行被测试的方法
        String resultClaimId = claimService.createClaim(customerId, claimDescription, claimAmount);

        // Assert:验证结果是否符合预期
        assertThat(resultClaimId).isEqualTo(claimId); // 验证返回的理赔ID正确
        assertThat(savedClaim.getStatus()).isEqualTo("PENDING"); // 验证状态正确
        assertThat(savedClaim.getAmount()).isEqualTo(claimAmount); // 验证金额正确

        // 验证依赖方法被正确调用(行为验证)
        verify(customerService, times(1)).getCustomer(customerId); // 客户服务被调用1次
        verify(claimRepository, times(1)).findByCustomerIdAndStatus(customerId, "PENDING"); // 检查重复申请被查询
        verify(claimRepository, times(1)).save(any(InsuranceClaim.class)); // 保存操作被调用1次
    }

    /**
     * 测试场景:当客户不存在时,应抛出业务异常
     */
    @Test
    void shouldThrowCustomerNotFoundExceptionWhenCustomerDoesNotExist() {
        // Arrange
        Long invalidCustomerId = 9999L;

        // 模拟:客户不存在,返回 null
        when(customerService.getCustomer(invalidCustomerId)).thenReturn(null);

        // Act & Assert:验证异常被正确抛出
        CustomerNotFoundException exception = assertThrows(
            CustomerNotFoundException.class,
            () -> claimService.createClaim(invalidCustomerId, "事故", 1000.0)
        );

        // 验证异常信息
        assertThat(exception.getMessage()).isEqualTo("客户ID 9999 不存在");
    }

    /**
     * 测试场景:当存在未处理的相同客户申请时,拒绝重复提交
     */
    @Test
    void shouldRejectDuplicateClaimWhenPendingClaimExists() {
        // Arrange
        Long customerId = 1002L;

        // 模拟:存在一个待处理的理赔申请
        InsuranceClaim existingClaim = new InsuranceClaim("CLAIM-2024-001", customerId, "旧申请", 2000.0, "PENDING", LocalDate.now());
        when(claimRepository.findByCustomerIdAndStatus(customerId, "PENDING"))
            .thenReturn(Optional.of(existingClaim));

        // Act & Assert:验证抛出业务异常
        DuplicateClaimException exception = assertThrows(
            DuplicateClaimException.class,
            () -> claimService.createClaim(customerId, "新申请", 3000.0)
        );

        assertThat(exception.getMessage()).isEqualTo("客户已有待处理的理赔申请,不能重复提交");
    }

    /**
     * 测试场景:当理赔金额为负数时,应抛出参数异常
     */
    @Test
    void shouldThrowIllegalArgumentExceptionWhenClaimAmountIsNegative() {
        // Arrange
        Long customerId = 1003L;
        double negativeAmount = -500.0;

        // Act & Assert
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> claimService.createClaim(customerId, "测试", negativeAmount)
        );

        assertThat(exception.getMessage()).isEqualTo("理赔金额不能为负数");
    }
}

补充说明:测试类结构说明

  • @ExtendWith(MockitoExtension.class):自动注入 @Mock 和 @InjectMocks,替代手动 MockitoAnnotations.openMocks(this)。
  • @Mock:创建模拟对象,不执行真实逻辑。
  • @InjectMocks:自动将模拟对象注入到被测类中(依赖注入)。
  • assertThrows():JUnit 5 断言异常抛出。
  • verify():验证模拟对象的方法是否被调用,次数是否正确。
  • assertThat().isEqualTo():来自 AssertJ,语法更自然,支持链式调用。

总结建议

  • 优先测试公有接口,通过路径覆盖间接验证私有方法。
  • 每个测试方法只测一个场景,命名清晰(如 should...when...)。
  • 不要测试 Spring 容器行为(如 @Autowired 是否生效),那是集成测试的范畴。
  • 保持测试快速:单元测试应在毫秒级完成,避免 I/O。
  • 覆盖率目标:核心业务逻辑 ≥ 80%,边界条件和异常路径必须覆盖。

✅ 在银行保险系统中,单元测试是保障理赔计算、客户验证、风控逻辑准确性的基石。
投入编写高质量单元测试,等于为你的代码买了“保险”。


测试、文档测试、单元测试 错误和测试 通常,程序员和程序开发人员花费大量时间进行调试和测试。很难给出准确的百分比,因为这在很大程度上取决于其他因素,例如个人编程风格、要解决的问题,当然还取决于程序员的资质。毫无疑问,编程语言是另一个重要因素。 正如古罗马人所知道的那样,您不必编写程序就会被错误所困扰。哲学家西塞罗在 2000 多年前创造了一个令人难忘的格言,经常被引用:“errare humanum est”* 这句格言经常被用作失败的借口。尽管几乎不可能完全消除软件产品中的所有错误,但我们应该始终为此雄心勃勃地工作,即将错 阅读详情

相关推荐

IDEA快速搭建一个SpringBoot Web项目、注入数据、单元测试、整合swagger

spring boot其实不是什么新的框架,它的设计是为了让你尽可能快的跑起来,它是基于Spring应用初始搭建,配置简洁明了。 一、创建Spring Boot 项目 1、选择File>New>project>Spring Initializr ,选择next 2、填写基本信息,继续next 3、选择Web>Spring Reactive Web 4、...

echoes_yu的博客 441

Java开发手册-3.单元测试

三、单元测试 【强制】好的单元测试必须遵守 AIR 原则。 说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上, 却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。 A:Automatic(自动化) I:Independent(独立性) R:Repeatable(可重复) 【强制】单元测试应该是全自动执行的,并且非交互式的。测试...

菜不是原罪 431

Spring Boot在Java领域的单元测试方法

单元测试是软件开发过程中的重要环节,它旨在对软件中的最小可测试单元进行检查和验证。在Spring Boot项目中,单元测试可以帮助开发者快速发现代码中的问题,提高代码的质量和可维护性。本文的目的是详细介绍Spring Boot在Java领域的各种单元测试方法,范围涵盖了从基础的测试框架使用到高级的模拟对象和集成测试技术。本文将按照以下结构进行组织:首先介绍核心概念与联系,包括JUnit、Mockito等测试框架与Spring Boot的关系;

AI开发架构师 1317

单元测试文档 软件单元描述,软件单元测试报告-模板

《软件单元测试报告-模板》由会员分享,可在线阅读,更多相关《软件单元测试报告-模板(8页珍藏版)》请在人人文库网上搜索。1、XXXXXX软件单元测试报告SRIJS-T0-/V0.0 XXXX年XX月姓名签名日期作者:审核:批准:序号修订内容简述修订日期修订前版本号修订后版本号修订人目 录1.介绍31.1目的31.2定义和缩写31.3参考资料32.单元测试策略32.1测试方法32.2测试工具32.3...

weixin_42503645的博客 586

Spring Boot 中如何使用单元测试

说明 因为Spring Boot 是一个Spring容器,所以一些测试方法需要启动后才能使用。如@Autowired。 Spring Boot Test 是在Spring Test之上的再次封装,增加了切片测试,增强了mock能力。 使用单元测试 添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</art

爱码士的博客 376

1.Spring Boot中单元测试详解

编写单元测试可以帮助开发人员编写高质量的代码,提升代码质量,减少Bug,便于重构。 `spring-boot-starter-test`也秉承了开箱即用的原则,集成了许多优秀的类库,可以很方便的帮助我们进行测试使用 spring-boot测试的核心包,一个为`spring-boot-test`包含核心项,另一个为`spring-boot-test-autoconfigure`支持了测试的自动配置

妖怪的博客 734

Spring Boot 单元测试

单元测试是软件开发中的一种关键测试类型,它是指对软件中的最小可测试单元进行检查和验证。对于面向对象编程,最小单元就是方法,独立的函数或过程也可以是最小单元。 在 Java 中,通常一个单元测试属于一种特定的测试工具框架,如 JUnit,它们可以很容易地插入到自动化构建过程或持续集成工具中。 单元测试的主要目标是隔离软件系统的各部分,并逐个测试。这将确保每个部分都按照预期工作。单元测试有助于提高软件质量,而且也使得代码在修改后更容易维护。

m0_62943934的博客 1万+

【Spring Boot】单元测试

单元测试在日常项目开发中必不可少,Spring Boot提供了完善的单元测试框架和工具用于测试开发的应用。接下来介绍Spring Boot为单元测试提供了哪些支持,以及如何在Spring Boot项目中进行单元测试。

衍生星球的博客 3万+

Spring Boot框架下的单元测试

及早发现并修复 BUG,使后续迭代功能或重构时能迅速验证不会破坏已实现的功能。

m0_73837751的博客 3075

junit单元测试文档

基于java语言的单元测试框架,本身不支持并行测试,需要对runner进行修改,作为单元测试的入门工具类书籍,这个文档还是挺不错的,欢迎多下载

单元测试文档单元测试文档

单元测试文档,经互联网下载整理所得。单元测试是软件测试很重要的一部分。

Spring Boot 基于 Mockito 单元测试

在网上刷到过“水货程序员”相关的帖子,列举了一些水货程序员的特征,其中一条就是不写单元测试,或者不知道单元测试是啥。看得瑟瑟发抖,完全不敢说话。在小公司里当开发,对单元测试根本没有要求,测试也就是本地启动服务,自己调下接口看看是否调通,以及和前端本地联调。毕业后入行以来都没写过,想写也不知道该怎么做。自己想摆脱“水货程序员”标签去写单元测试,也只是照着网上博客,本地写一写,不知道写得是否规范,所以从没提交过单元测试代码。后面跳槽,项目有要求写单元测试了,这就有能够参考的单元测试代码了。

1929

单元测试说明模板

本文档为XX项目的单元测试活动提供测试设计规格及测试用例规格。文档内容包括了需要测试的类、测试使用的模型、针对每个类的测试策略以及所需执行的测试用例等。

Spring Boot干货系列:(十二)Spring Boot使用单元测试

前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求Service层单元测试Controller层单元测试新断言assertThat使用单元测试的回滚正文Spring Boot中引入单元测试很简单,依赖如下:本篇实例Spring Boot版本为1.5.9.RELEASE,引入spring-boot-starter-test后,有如下几个库: •

嘟爷java超神学堂 5233

零售-公司间关联交易退货STO的实现方式

零售-公司间关联交易采购 STO-退货 集团公司下经常会出现一家公司和另外一家公司的公司间的关联交易,如跨公司销售、总分公司之间采购,分公司之间的采购等业务;通常公司间采购会采取STO的模式来实现(根据业务模式可以将库存转储统一做成STO的模式,公司间STO、同公司工厂间、同工厂库存地点间 的STO SAP都是支持的); 公司间STO 正向A向B采购都很熟悉,公司间退货STO的其实也是比较普遍的业务现象,很多实施着也将其做成反向的STO(B向A采购),而不是退货STO(A向B,item 选择return的标

weixin_45518759的博客 2830

Simscape Multiboby学习笔记1——跟着官网学Multibody

文章目录一. 引言二. Simscape Multibody自学之路三. 本教程的目录 一. 引言 最近在倒腾Matlab的动力学仿真,起因是看到有人推荐Matlab的multbody模块,一经了解之下简直惊为天人。作为一名Matlab的小粉,看到这么好用的模块简直不要太开心,之前用simulink搭飞行器的仿真模型,虽然能够仿真看到六自由度位移,毕竟不能可视化,还是很不够直观,而且simulink仿真(以及更费力的脚本文件仿真)需要精确建模,模型不精确计算也是天差地别。而Multibody在具备其他动力学

qq_30267617的博客 8240
上一篇: 使用 Podman Pod 搭建 ELK(Elasticsearch + Logstash + Kibana)完整实战指南
下一篇: SpringBoot单元测试全面解析
龙茶清欢
博客等级 码龄7年 3720粉丝 375原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值