学透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的使用。

相关推荐
陌路物是人非1 分钟前
SpringBoot + Netty + Vue + WebSocket实现在线聊天
vue.js·spring boot·websocket·netty
穿林鸟2 小时前
Spring Boot项目信创国产化适配指南
java·spring boot·后端
伏游2 小时前
【BUG】生产环境死锁问题定位排查解决全过程
服务器·数据库·spring boot·后端·postgresql·bug
hycccccch3 小时前
Springcache+xxljob实现定时刷新缓存
java·后端·spring·缓存
爱的叹息3 小时前
SpringBoot集成Redis 灵活使用 TypedTuple 和 DefaultTypedTuple 实现 Redis ZSet 的复杂操作
spring boot·redis·bootstrap
wisdom_zhe3 小时前
Spring Boot 日志 配置 SLF4J 和 Logback
java·spring boot·logback
揣晓丹3 小时前
JAVA实战开源项目:校园失物招领系统(Vue+SpringBoot) 附源码
java·开发语言·vue.js·spring boot·开源
鸭梨大大大3 小时前
Spring Web MVC入门
前端·spring·mvc
侧耳倾听1113 小时前
使用内存数据库来为mapper层的接口编写单元测试
数据库·单元测试
顽疲3 小时前
从零用java实现 小红书 springboot vue uniapp (11)集成AI聊天机器人
java·vue.js·spring boot·ai