学透Spring Boot — 010. 单元测试和Spring Test

系列文章目录

这是CSDN postnull 博客《学透Spring Boot》系列的一篇,更多文章请移步:Postnull - 学透Spring Boot系列文章


文章目录

  • 系列文章目录
  • 前言
  • [1. 基本概念](#1. 基本概念)
  • [3. Spring Test](#3. Spring Test)
    • [为什么要用Spring Test](#为什么要用Spring Test)
    • [引入Spring Test](#引入Spring Test)
    • [Spring Test最简单使用](#Spring Test最简单使用)

前言

开发的过程中,写业务代码是工作的一部分,测试也是工作的一部分。尤其是作为开发人员,用单元测试来测试我们的代码,可以更早的发现bug。

1. 基本概念

UT 单元测试

单元测试 Unit Testing,通常是开发的一部分,也是开发过程中最小的测试单元。主要测试一个类或一个方法的逻辑。

因为只是保证我们自己写的代码的逻辑,所以是不涉及外部服务的(比如数据库和外部接口),外部服务我们通常使用mock来模拟它们的行为。

UT是代码片段的测试
外部集成测试才需要依赖真正的外部服务

TDD 测试驱动开发

近些年流行很多种 XDD,其中和测试相关的就是TDD,Test driven development,这是一种开发方法。

以前传统的开发模式,我们先一口气写完业务代码,然后再开始写单元测试。但是会遇到一些问题:

  • 业务代码不方便测试
  • 测试时才发现bug,导致业务代码要做很大的调整

所以我们可以换一种思路,先写测试用例,再写业务代码。

这也就是TDD的三个步骤,遵循 红-绿-重构(Red-Green-Refactor)

  1. 红:编写一个失败的测试。
  2. 绿:编写代码使测试通过。
  3. 重构:优化代码,确保测试仍然通过。

我们先快速试一下

先写个测试用例

java 复制代码
public class CalculatorTest {
    @Test
    public void testAdd1() {
        Calculator calculator = new Calculator();
        assertEquals(3, calculator.add(1, 2));
    }

    @Test
    public void testAdd2() {
        Calculator calculator = new Calculator();
        assertEquals(0, calculator.add(1, -1));
    }
}

这个时候,测试用例是通不过的,因为我们的业务代码类都还没实现,编译错误。当然这样体验不太好,我们也可以先实现业务代码的骨架,只是空实现。

java 复制代码
public class Calculator {
    public Integer add(int a, int b) {
        return null;
    }
}

我们运行一下测试用例,UT不通过,这也是预期的。

然后我们开始实现代码

java 复制代码
public class Calculator {
    public Integer add(int a, int b) {
        return 0;
    }
}

这时候单元用例部分通过了

说明我们的代码有问题,继续调整

java 复制代码
public class Calculator {
    public Integer add(int a, int b) {
        return a + b;
    }
}

这次我们的UT都过了,表示代码基本没问题了。(基本没问题不表示绝对没问题,因为可能测试覆盖率不够,有些边缘的case没有考虑到。)

下次,我们的业务代码被修改了,理论上出了bug,我们的测试用例是要失败的。

java 复制代码
public class Calculator {
    public Integer add(int a, int b) {
        if(a < 0 || b < 0){
            return null;
        }
        return a + b;
    }
}

这时候,我们就需要注意了,是代码有问题,还是测试用例需要更新。

转换思路------测试先行

UT测试框架

Java最常用的 UT 框架有两个

  1. Junit
  2. TestNG

其中Junit比较轻量,满足大部分场景。TestNG功能更强大,比如支持并行测试(同时运行多个测试用例,Junit只能一个完了后再跑下一个)。具体对比:

  • TestNG 的@DataProvider,注入测试数据更方便,Junit5后提供的@ParameteriedTest也差不多功能
  • TestNG支持测试依赖,运行A用例前会自动先触发B用例
  • TestNG支持测试分组,方便运行一组测试用例
  • TestNG支持用例并行执行,对相对独立的case可以大大加快UT的时间
  • ......

TestNG更强大,但有时简单就好

Mock框架

前面也说过,对于外部依赖,甚至是业务代码其它的类,我们都应该模拟,这样保证我们可以关注被测试的这个类和这个方法。
保证测试用例的能够独立、快速的被测试

有很多Mock框架

  • Mockito:最常用
  • PowerMock:兼容Mockito,功能更强大,比如测试静态方法和私有方法(反思:private方法真的应该被测试吗)

比如使用Mockito,步骤基本都是差不多的,先创建mock对象和行为,然后调用方法,最后验证结果

java 复制代码
public class UserServiceTest {
    @Test
    public void testGetUser() {
        // 1. 创建 Mock 对象
        UserRepository mockRepo = mock(UserRepository.class);

        // 2. 设置 Mock 行为
        when(mockRepo.findById(1)).thenReturn(new User(1, "John"));

        // 3. 调用测试方法
        UserService userService = new UserService(mockRepo);
        User user = userService.getUser(1);

        // 4. 验证结果
        assertEquals("John", user.getName());
    }
}

3. Spring Test

为什么要用Spring Test

对于Java项目,直接使用TestNG/Junit + Mockito 不就可以了吗?为什么还要用 Spring Test?

以前也有这个疑问,但是如果我们测试Spring 项目时,Junit + Mockito 就有点力不从心了。比如:

  • 注入Spring的配置,比如application-test.xml中配置,注入到Spring容器中去
  • 自动注入Bean,这个做不到,得手动new对象
  • 模拟HTTP请求,这个也做不到,所以Controller测试不了
  • 测试Spring整个应用,也做不到,只能测试单个类

Spring Test就是为了更方便的测试Spring 应用

当然,Spring Test不是要取代 TestNG/Junit + Mockito, 想法,它底层用的还是这些技术。

它只是提供了一堆工具和注解,帮助我们更方便测试Spring应用。

引入Spring Test

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

我们在关联依赖中也可以看到,它自动引入了Junit和Mockito

这框架本身包含了两个模块

  1. Spring Test 核心模块 spring-boot-test
  2. 自动配置模块 spring-boot-test-autoconfigure: 我们很多自动配置,比如注入

    比如在自动配置的模块中,我们可以看到MockMvc的自动配置。这个类我们以后测试会经常遇到。

Spring Test最简单使用

本文我们先来个最简单的例子,测试我们的Controller。

java 复制代码
@RestController
@RequestMapping("/tn-users")
public class TnUserController {
    private TnUserService tnUserService;

    public TnUserController(TnUserService tnUserService) {
        this.tnUserService = tnUserService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<TypiUser> getUser(@PathVariable int id) {
        TypiUser user = tnUserService.getUserById(id);
        return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
    }
}

然后我们编写测试用例

java 复制代码
@WebMvcTest(TnUserController.class)
public class TnUserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private TnUserService tnUserService;

    @Test
    @DisplayName("测试成功查询用户的情况")
    public void testGetUser() throws Exception {
        //given
        TypiUser mockUser = TypiUser.builder()
                .id(1234)
                .name("Joe")
                .build();

        //when
        when(tnUserService.getUserById(eq(1234))).thenReturn(mockUser);

        //then
        mockMvc.perform(get("/tn-users/{id}", 1234))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1234))
                .andExpect(jsonPath("$.name").value("Joe"));
    }
}

测试通过

稍微改一下代码,再次运行,会发现报错。

java 复制代码
        mockMvc.perform(get("/tn-users/{id}", 1234))
                .andExpect(status().isBadRequest());

可以看到,我们现在具备测试Controller层的能力了。如果没有Spring Test,只是靠Mockito基本做不到接口层的测试。

我们先不用关注实现的细节。下一篇文章我们会全面介绍Spring Test的使用。

相关推荐
忠于明白9 分钟前
Spring AI 核心工作流
人工智能·spring·大模型应用开发·spring ai·ai 应用商业化
KK溜了溜了1 小时前
JAVA-springboot log日志
java·spring boot·logback
有梦想的攻城狮2 小时前
spring中的@RabbitListener注解详解
java·后端·spring·rabbitlistener
hello早上好2 小时前
BeanFactory 实现
后端·spring·架构
我命由我123452 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开2 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy2 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神2 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋3 小时前
Spring Boot
java·前端·spring boot
howard20054 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis