searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

基于SpringBoot实现单元测试的多种情境/方法(一)

2022-12-05 03:35:45
128
0

1 什么是单元测试(UT)?

最近工作涉及到给一些代码写单元测试,这可谓是功能上线前评判代码质量极其重要的一步,当然单元测试之后还有系统测试、集成测试等,这些测试的关系和意义不是本文的重点内容,并且比较好理解,这里贴一个链接大家可以自行按需阅读:https://blog.csdn.net/u013185349/article/details/123396943

总的来看单元测试有三个核心要点:

1.测试范围:代码中的每一个小的单元(一般是类/方法)
单元测试不测试模块或系统。一般都是由开发工程师而非测试工程师完成的,是代码层面的测试,用于测试“自己”编写的代码的正确性。

2.测试依赖:不依赖于任何不可控组建
单元测试不依赖于任何不可控的组件,即使代码中依赖了其他这些不可控的组件(比如复杂外部系统,复杂模块或类),也需要通过 mock 的方式将其变成可控。

3.测试意义:得到预期的输入输出
在你写完一个功能代码之后,怎么保证你的代码运行正确?在各种异常(数据异常、输入异常、调用异常等)情况下,程序运行结果都符合你预先设计的预期,返回合适的报错呢?这个时候,单元测试就派上了用场

2 单元测试的工具

常用的单元测试工具有三种,在这里我们都会介绍,从基础到进阶的用法慢慢都会写到

JUnit

经典中的经典,相信哪怕是入门java的学习者,绝大部分也使用过JUnit的@Test注解来进行单元测试,但目前已经更新到JUnit5,很多属性是有变化的,SpringBoot默认集成的也是JUnit5版本,所以本文可能会与一些网上能找到的资料有所不同

Asserts

就是我们常说的“断言”,也是非常简单好用的工具,功能强悍,支持各种断言方法

Mockito

Mock可能对于部分初学者来说比较陌生,所以这里多说一些:JUnit固然很简单方便,但如果在单元测试的过程中吗,需要构建请求头和查询参数、构建复杂的脚本等特殊需求,用标准的JUnit是无法实现的,这个时候我们就要用到MockMVC

什么是mock?

即mock object,模拟对象,是在OOP中模拟真实对象行为的假对象,在单元测试中有时无法使用真实对象,因此需要用到模拟对象来测试。

比如在以下情况可以采用模拟对象来替代真实对象:
1.真实对象的行为是不确定的 (例如,当前的时间或温度) ;
2.真实对象很难搭建起来;
3.真实对象的行为很难触发 (例如,网络错误) ;
4.真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化) ;
5.真实的对象是用户界面, 或包括用户界面在内;
6.真实的对象使用了回调机制:
7.真实对象可能还不存在:
8.真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

举例:只做了一点简单的更改,但是验证需要重启底层资源,一等就是五六分钟;要模拟在某个操作系统/浏览器环境下的功能表现,总不能专门去搭一套环境吧

因此spring给我们提供了Mockito工具,即模拟对象的生成器,开发分为三个步骤:
1.模拟外部依赖,比如我们需要的底层数据、网络请求头等
2.执行具体的测试代码
3.验证产生的结果与预期是否相符

spring-test包提供了一个核心的对象——MockMVC,MockMvc是由spring-test包提供,实现了对Htp请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快.不依赖网络环境。同时提供了一套验证的工具, 结果的验证十分方便。

3 编写单元测试的原则

一般来说,单元测试需要遵循3A模式(Arrange/Act/Assert)来编写具体的代码,具体的概念可以参考:https://juejin.cn/post/7005448543192252423

实际上3A是一个简单实用的概念,举个简单的例子:

Arrange:创建测试所需要的实例

Act:运行你所需要测试的具体行动(方法)

Assert:判断返回的是否与预期一样

4 Junit 5 实现基础单元测试

1.建立相应的包存储测试代码
实际上Spring Boot对Junit进行了整合,我们可以从工程架构中看到,创建一个spring工程后(springinitializer),会自带一个src下的test包和一个默认测试的代码(如下图),如果没有的话,也可以自己创建一个(因为有些开发者可能创建的是maven工程)

2.引入依赖
当然要想使用这个测试,我们当然要引入相关的依赖,一般来说只要你创建了springboot项目,它是自动导入的。但是需要注意的是,我们通常需要排除junit-vintage-engine这个依赖,为什么呢?
如果不排除这个依赖,用的不会是spring整合的Junit,从而后面写测试代码的时候可能需要加RunWith注解来指定是用哪一个Spring整合的Junit

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

3.编写测试代码
这里我们可以参考给我们的默认测试代码来写,默认代码会是这种格式(@SpringBootTest就是spring整合了junit之后的一个注解):

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Springboot01ApplicationTests {

    @Test
    void contextLoads() {
    }

}

我们这里简单写一下,新建一个User类,来让测试代码输出User的某个参数

package wy.springboot01.domain;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

/**
 * springboot项目启动的时候,自动将application.yml内容加载到实体对象中
 */

@Data
//将实体类交给spring管理,自动扫描
@Component
public class User {
    private Integer uid;
    private String uname;
    private String password;
    private ArrayList<String> addrs;

    public User() {
    }

    public User(Integer uid, String uname, String password, ArrayList<String> addrs) {
        this.uid = uid;
        this.uname = uname;
        this.password = password;
        this.addrs = addrs;
    }
}

单元测试代码:

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import wy.springboot01.domain.User;

@SpringBootTest(classes = {User.class})
class Springboot01ApplicationTests {

    @Autowired
    private User user;
    @Test
    void contextLoads() {
        System.out.println(user.getAddrs());
    }

}

4.直接运行测试代码,可能会失败,常见错误是:Found multiple @SpringBootConfiguration annotated classes,这是因为之前在做其他代码的时候,可能有多个文件用了@SpringBootConfiguration从而导致冲突,只需要注释掉多余的只留一个即可,或者指定你要用哪个@SpringBootConfiguration来测试,因此我们这里用了:@SpringBootTest(classes = {User.class})

5.正常会输出null,因为我们并没有给Addrs这个参数赋默认值,到这里我们就完成了一个没有3A的最后一个A(断言)的单元测试

6.但是我们在开发中通常不会只测试某一个类的参数,比如我们想测试在某个启动类中(某个功能加载的过程中)User对象的值是否载入成功,那么也可以按如下来实现:
首先我们先写一个yml文件来给User赋默认值,就比如叫做application.yml

user:
  uid: 1998051110
  uname: wuyu
  password: wuyu1999
  addrs:
    - Beijing
    - Sichuan
    - Nanchang

然后给User类加上注解:

//加载配置内容,设定配置前缀,注意:prefix参数不支持小驼峰原则,必须全部小写
@ConfigurationProperties(prefix = "user")

最后把测试的启动类改成这个工程的默认启动类:

@SpringBootTest(classes = {Springboot01Application.class})

这样启动之后我们就会得到如下结果,默认值加载成功了:

[Beijing, Sichuan, Nanchang]

7.那么如何给结果做一个断言呢?换而言之我不需要用人眼去判断结果是不是对的,系统就西东判断了,也很简单,这里我们就要介绍到我们的第二种测试工具:asserts

5 Asserts方法判断返回值

实际上我们在日常写代码中有时候也会用到assert,比如判断某个入参是否为空,直接使用:assert data.length != 0; 即可

那么回到单元测试,我们如何判断返回值?

//判断方法返回是否为Flase
Assertions.assertFalse(SomeClass.someMethod());
//判断方法返回是否为True
Assertions.assertTure(SomeClass.someMethod());
//判断方法返回是否与对应值相等
Assertions.assertTure("预期返回", SomeClass.someMethod());

同样将刚刚的Juint判断user的例子扩充一下,我们判断用户名是否与预期一致,可以写成:

@SpringBootTest(classes = {Springboot01Application.class})
class Springboot01ApplicationTests {

    @Autowired
    private User user;
    @Test
    void contextLoads() {
        Assertions.assertArrayEquals(String.join("","Beijing", "Sichuan", "Nanchang" ).toCharArray()
                ,String.join("",user.getAddrs() ).toCharArray());
    }

}

最终运行结果如下图,就实现了不由人眼判断而是系统判断,非常方便:

6 MockMVC

刚刚说过,mock就是用来模拟一些不太好去创建的代码外部依赖,最典型的就是对于controller的测试,传统的方法是代码起来之后使用postman,而单元测试要求我们不能有这种外部依赖,因此我们就用mock来模拟这样的一个环境

还是拿user类来举例,比如我们有一个controller长这个样子,如果我们要验证他的返回是不是对的,按照常理我们应该去启动spring并且用浏览器、postman、http脚本等来看看它的返回是什么,非常的浪费时间

@GetMapping("/user")
public ArrayList<User> getUser(){
    System.out.println("user get......");
    ArrayList<User> users = new ArrayList<>();
    users.add(new User(1001,"wu","1212",new ArrayList<>(Arrays.asList("nanchang", "sichuan", "beijing"))));
    users.add(new User(1002,"du","1313",new ArrayList<>(Arrays.asList("chang", "sica", "beng"))));
    return users;
}

那么用mock怎么去做呢?
首先我们先在test包下,创建一个与默认测试代码平行的测试类,比如就叫MockMVCTester,如下:

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.annotation.Resource;
//进行每一次mock模拟tomcat容器的时候,使用随机端口启动,这样不会有端口占用的问题
@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//自动配置以及启用mvc对象
@AutoConfigureMockMvc
public class MockMVCTester {
    //注入MockMVC对象,它是springtest依赖中自带的
    @Resource
    private MockMvc mockMvc;
    @Test
    public void testMock() throws Exception {
        //获取mock返回的对象
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user"))//perform模拟一个http请求,这里是get方法
                .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器返回的是200
                .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
                .andReturn();//将结果返回出来
    }
}

启动测试方法testMock,可以发现:

同时也会返回controller的信息,可以对照是否符合预期,这样我们就实现了不需要启动浏览器、postman等即可测试controller

但我们还可以更加便利一些,直接在运行的时候就判断它的返回是不是符合预期,本例的返回为:
[{"uid":1001,"uname":"wu","password":"1212","addrs":["nanchang","sichuan","beijing"]},{"uid":1002,"uname":"du","password":"1313","addrs":["chang","sica","beng"]}]
因此我们将测试代码做一些改动

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.annotation.Resource;
//进行每一次mock模拟tomcat容器的时候,使用随机端口启动,这样不会有端口占用的问题
@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//自动配置以及启用mvc对象
@AutoConfigureMockMvc
public class MockMVCTester {
    //注入MockMVC对象,它是springtest依赖中自带的
    @Resource
    private MockMvc mockMvc;
    @Test
    public void testMock() throws Exception {
        //获取mock返回的对象
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user"))//perform模拟一个http请求,这里是get方法
                .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器返回的是200
                .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
                .andExpect(MockMvcResultMatchers.content().string("[{\"uid\":1001,\"uname\":\"wu\"," +
                        "\"password\":\"1212\",\"addrs\":[\"nanchang\",\"sichuan\",\"beijing\"]}," +
                        "{\"uid\":1002,\"uname\":\"du\",\"password\":\"1313\",\"addrs\"" +
                        ":[\"chang\",\"sica\",\"beng\"]}]"))//content表示对于返回的请求体数据进行判断,string表示进行比对
                .andReturn();//将结果返回出来
    }
}

再次启动,运行依然成功说明比对正确,返回符合预期,如果这个时候我们改了controller的逻辑,则返回“test fail”,非常好用,甚至会比对预期值和实际值

注:如果觉得testMock()看起来不舒服,可以在@Test下面加注解@DisplayName("get方法测试用例"),来自定义测试方法名称
如果有入参、且返回是一个json可以参考如下案例:

@Test
@DisplayName("get方法+有入参+有json返回")
public void testMock1() throws Exception {
    //mock返回的对象可以不获取,因为单纯的判断对错用不上
    mockMvc.perform(MockMvcRequestBuilders.get("/user/para")//perform模拟一个http请求,这里是get方法
                    .header("token", "akakak")//请求头
                    .param("id","wy")//请求参数
                    .param("password","asd"))//请求参数
            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200
            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
            .andExpect(MockMvcResultMatchers.jsonPath("ak").value("asd"))//获取返回的json并核对对应的值是否一样
            .andReturn();//将结果返回出来
}

如果方法为post,可以参考如下案例

@Test
@DisplayName("post方法测试用例")
public void testMock1() throws Exception {
    //IoC
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("IoC.xml");
    User user = context.getBean(User.class);
    ObjectMapper mapper = new ObjectMapper();
    user.setUname("wy");
    //mock返回的对象可以不获取,因为单纯的判断对错用不上
    mockMvc.perform(MockMvcRequestBuilders.post("/user")//perform模拟一个http请求,这里是get方法
                    .content(mapper.writeValueAsString(user))//用IoC建立一个User对象
                    .contentType(MediaType.APPLICATION_JSON_VALUE))//添加json类数据,转化为入参
            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200
            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
            .andExpect(MockMvcResultMatchers.jsonPath("uname").value("wy"))//获取返回的json并核对对应的值是否一样
            .andReturn();//将结果返回出来
}
0条评论
0 / 1000
才开始学技术的小白
23文章数
2粉丝数
才开始学技术的小白
23 文章 | 2 粉丝
原创

基于SpringBoot实现单元测试的多种情境/方法(一)

2022-12-05 03:35:45
128
0

1 什么是单元测试(UT)?

最近工作涉及到给一些代码写单元测试,这可谓是功能上线前评判代码质量极其重要的一步,当然单元测试之后还有系统测试、集成测试等,这些测试的关系和意义不是本文的重点内容,并且比较好理解,这里贴一个链接大家可以自行按需阅读:https://blog.csdn.net/u013185349/article/details/123396943

总的来看单元测试有三个核心要点:

1.测试范围:代码中的每一个小的单元(一般是类/方法)
单元测试不测试模块或系统。一般都是由开发工程师而非测试工程师完成的,是代码层面的测试,用于测试“自己”编写的代码的正确性。

2.测试依赖:不依赖于任何不可控组建
单元测试不依赖于任何不可控的组件,即使代码中依赖了其他这些不可控的组件(比如复杂外部系统,复杂模块或类),也需要通过 mock 的方式将其变成可控。

3.测试意义:得到预期的输入输出
在你写完一个功能代码之后,怎么保证你的代码运行正确?在各种异常(数据异常、输入异常、调用异常等)情况下,程序运行结果都符合你预先设计的预期,返回合适的报错呢?这个时候,单元测试就派上了用场

2 单元测试的工具

常用的单元测试工具有三种,在这里我们都会介绍,从基础到进阶的用法慢慢都会写到

JUnit

经典中的经典,相信哪怕是入门java的学习者,绝大部分也使用过JUnit的@Test注解来进行单元测试,但目前已经更新到JUnit5,很多属性是有变化的,SpringBoot默认集成的也是JUnit5版本,所以本文可能会与一些网上能找到的资料有所不同

Asserts

就是我们常说的“断言”,也是非常简单好用的工具,功能强悍,支持各种断言方法

Mockito

Mock可能对于部分初学者来说比较陌生,所以这里多说一些:JUnit固然很简单方便,但如果在单元测试的过程中吗,需要构建请求头和查询参数、构建复杂的脚本等特殊需求,用标准的JUnit是无法实现的,这个时候我们就要用到MockMVC

什么是mock?

即mock object,模拟对象,是在OOP中模拟真实对象行为的假对象,在单元测试中有时无法使用真实对象,因此需要用到模拟对象来测试。

比如在以下情况可以采用模拟对象来替代真实对象:
1.真实对象的行为是不确定的 (例如,当前的时间或温度) ;
2.真实对象很难搭建起来;
3.真实对象的行为很难触发 (例如,网络错误) ;
4.真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化) ;
5.真实的对象是用户界面, 或包括用户界面在内;
6.真实的对象使用了回调机制:
7.真实对象可能还不存在:
8.真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

举例:只做了一点简单的更改,但是验证需要重启底层资源,一等就是五六分钟;要模拟在某个操作系统/浏览器环境下的功能表现,总不能专门去搭一套环境吧

因此spring给我们提供了Mockito工具,即模拟对象的生成器,开发分为三个步骤:
1.模拟外部依赖,比如我们需要的底层数据、网络请求头等
2.执行具体的测试代码
3.验证产生的结果与预期是否相符

spring-test包提供了一个核心的对象——MockMVC,MockMvc是由spring-test包提供,实现了对Htp请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快.不依赖网络环境。同时提供了一套验证的工具, 结果的验证十分方便。

3 编写单元测试的原则

一般来说,单元测试需要遵循3A模式(Arrange/Act/Assert)来编写具体的代码,具体的概念可以参考:https://juejin.cn/post/7005448543192252423

实际上3A是一个简单实用的概念,举个简单的例子:

Arrange:创建测试所需要的实例

Act:运行你所需要测试的具体行动(方法)

Assert:判断返回的是否与预期一样

4 Junit 5 实现基础单元测试

1.建立相应的包存储测试代码
实际上Spring Boot对Junit进行了整合,我们可以从工程架构中看到,创建一个spring工程后(springinitializer),会自带一个src下的test包和一个默认测试的代码(如下图),如果没有的话,也可以自己创建一个(因为有些开发者可能创建的是maven工程)

2.引入依赖
当然要想使用这个测试,我们当然要引入相关的依赖,一般来说只要你创建了springboot项目,它是自动导入的。但是需要注意的是,我们通常需要排除junit-vintage-engine这个依赖,为什么呢?
如果不排除这个依赖,用的不会是spring整合的Junit,从而后面写测试代码的时候可能需要加RunWith注解来指定是用哪一个Spring整合的Junit

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

3.编写测试代码
这里我们可以参考给我们的默认测试代码来写,默认代码会是这种格式(@SpringBootTest就是spring整合了junit之后的一个注解):

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Springboot01ApplicationTests {

    @Test
    void contextLoads() {
    }

}

我们这里简单写一下,新建一个User类,来让测试代码输出User的某个参数

package wy.springboot01.domain;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

/**
 * springboot项目启动的时候,自动将application.yml内容加载到实体对象中
 */

@Data
//将实体类交给spring管理,自动扫描
@Component
public class User {
    private Integer uid;
    private String uname;
    private String password;
    private ArrayList<String> addrs;

    public User() {
    }

    public User(Integer uid, String uname, String password, ArrayList<String> addrs) {
        this.uid = uid;
        this.uname = uname;
        this.password = password;
        this.addrs = addrs;
    }
}

单元测试代码:

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import wy.springboot01.domain.User;

@SpringBootTest(classes = {User.class})
class Springboot01ApplicationTests {

    @Autowired
    private User user;
    @Test
    void contextLoads() {
        System.out.println(user.getAddrs());
    }

}

4.直接运行测试代码,可能会失败,常见错误是:Found multiple @SpringBootConfiguration annotated classes,这是因为之前在做其他代码的时候,可能有多个文件用了@SpringBootConfiguration从而导致冲突,只需要注释掉多余的只留一个即可,或者指定你要用哪个@SpringBootConfiguration来测试,因此我们这里用了:@SpringBootTest(classes = {User.class})

5.正常会输出null,因为我们并没有给Addrs这个参数赋默认值,到这里我们就完成了一个没有3A的最后一个A(断言)的单元测试

6.但是我们在开发中通常不会只测试某一个类的参数,比如我们想测试在某个启动类中(某个功能加载的过程中)User对象的值是否载入成功,那么也可以按如下来实现:
首先我们先写一个yml文件来给User赋默认值,就比如叫做application.yml

user:
  uid: 1998051110
  uname: wuyu
  password: wuyu1999
  addrs:
    - Beijing
    - Sichuan
    - Nanchang

然后给User类加上注解:

//加载配置内容,设定配置前缀,注意:prefix参数不支持小驼峰原则,必须全部小写
@ConfigurationProperties(prefix = "user")

最后把测试的启动类改成这个工程的默认启动类:

@SpringBootTest(classes = {Springboot01Application.class})

这样启动之后我们就会得到如下结果,默认值加载成功了:

[Beijing, Sichuan, Nanchang]

7.那么如何给结果做一个断言呢?换而言之我不需要用人眼去判断结果是不是对的,系统就西东判断了,也很简单,这里我们就要介绍到我们的第二种测试工具:asserts

5 Asserts方法判断返回值

实际上我们在日常写代码中有时候也会用到assert,比如判断某个入参是否为空,直接使用:assert data.length != 0; 即可

那么回到单元测试,我们如何判断返回值?

//判断方法返回是否为Flase
Assertions.assertFalse(SomeClass.someMethod());
//判断方法返回是否为True
Assertions.assertTure(SomeClass.someMethod());
//判断方法返回是否与对应值相等
Assertions.assertTure("预期返回", SomeClass.someMethod());

同样将刚刚的Juint判断user的例子扩充一下,我们判断用户名是否与预期一致,可以写成:

@SpringBootTest(classes = {Springboot01Application.class})
class Springboot01ApplicationTests {

    @Autowired
    private User user;
    @Test
    void contextLoads() {
        Assertions.assertArrayEquals(String.join("","Beijing", "Sichuan", "Nanchang" ).toCharArray()
                ,String.join("",user.getAddrs() ).toCharArray());
    }

}

最终运行结果如下图,就实现了不由人眼判断而是系统判断,非常方便:

6 MockMVC

刚刚说过,mock就是用来模拟一些不太好去创建的代码外部依赖,最典型的就是对于controller的测试,传统的方法是代码起来之后使用postman,而单元测试要求我们不能有这种外部依赖,因此我们就用mock来模拟这样的一个环境

还是拿user类来举例,比如我们有一个controller长这个样子,如果我们要验证他的返回是不是对的,按照常理我们应该去启动spring并且用浏览器、postman、http脚本等来看看它的返回是什么,非常的浪费时间

@GetMapping("/user")
public ArrayList<User> getUser(){
    System.out.println("user get......");
    ArrayList<User> users = new ArrayList<>();
    users.add(new User(1001,"wu","1212",new ArrayList<>(Arrays.asList("nanchang", "sichuan", "beijing"))));
    users.add(new User(1002,"du","1313",new ArrayList<>(Arrays.asList("chang", "sica", "beng"))));
    return users;
}

那么用mock怎么去做呢?
首先我们先在test包下,创建一个与默认测试代码平行的测试类,比如就叫MockMVCTester,如下:

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.annotation.Resource;
//进行每一次mock模拟tomcat容器的时候,使用随机端口启动,这样不会有端口占用的问题
@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//自动配置以及启用mvc对象
@AutoConfigureMockMvc
public class MockMVCTester {
    //注入MockMVC对象,它是springtest依赖中自带的
    @Resource
    private MockMvc mockMvc;
    @Test
    public void testMock() throws Exception {
        //获取mock返回的对象
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user"))//perform模拟一个http请求,这里是get方法
                .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器返回的是200
                .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
                .andReturn();//将结果返回出来
    }
}

启动测试方法testMock,可以发现:

同时也会返回controller的信息,可以对照是否符合预期,这样我们就实现了不需要启动浏览器、postman等即可测试controller

但我们还可以更加便利一些,直接在运行的时候就判断它的返回是不是符合预期,本例的返回为:
[{"uid":1001,"uname":"wu","password":"1212","addrs":["nanchang","sichuan","beijing"]},{"uid":1002,"uname":"du","password":"1313","addrs":["chang","sica","beng"]}]
因此我们将测试代码做一些改动

package wy.springboot01;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.annotation.Resource;
//进行每一次mock模拟tomcat容器的时候,使用随机端口启动,这样不会有端口占用的问题
@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//自动配置以及启用mvc对象
@AutoConfigureMockMvc
public class MockMVCTester {
    //注入MockMVC对象,它是springtest依赖中自带的
    @Resource
    private MockMvc mockMvc;
    @Test
    public void testMock() throws Exception {
        //获取mock返回的对象
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user"))//perform模拟一个http请求,这里是get方法
                .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器返回的是200
                .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
                .andExpect(MockMvcResultMatchers.content().string("[{\"uid\":1001,\"uname\":\"wu\"," +
                        "\"password\":\"1212\",\"addrs\":[\"nanchang\",\"sichuan\",\"beijing\"]}," +
                        "{\"uid\":1002,\"uname\":\"du\",\"password\":\"1313\",\"addrs\"" +
                        ":[\"chang\",\"sica\",\"beng\"]}]"))//content表示对于返回的请求体数据进行判断,string表示进行比对
                .andReturn();//将结果返回出来
    }
}

再次启动,运行依然成功说明比对正确,返回符合预期,如果这个时候我们改了controller的逻辑,则返回“test fail”,非常好用,甚至会比对预期值和实际值

注:如果觉得testMock()看起来不舒服,可以在@Test下面加注解@DisplayName("get方法测试用例"),来自定义测试方法名称
如果有入参、且返回是一个json可以参考如下案例:

@Test
@DisplayName("get方法+有入参+有json返回")
public void testMock1() throws Exception {
    //mock返回的对象可以不获取,因为单纯的判断对错用不上
    mockMvc.perform(MockMvcRequestBuilders.get("/user/para")//perform模拟一个http请求,这里是get方法
                    .header("token", "akakak")//请求头
                    .param("id","wy")//请求参数
                    .param("password","asd"))//请求参数
            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200
            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
            .andExpect(MockMvcResultMatchers.jsonPath("ak").value("asd"))//获取返回的json并核对对应的值是否一样
            .andReturn();//将结果返回出来
}

如果方法为post,可以参考如下案例

@Test
@DisplayName("post方法测试用例")
public void testMock1() throws Exception {
    //IoC
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("IoC.xml");
    User user = context.getBean(User.class);
    ObjectMapper mapper = new ObjectMapper();
    user.setUname("wy");
    //mock返回的对象可以不获取,因为单纯的判断对错用不上
    mockMvc.perform(MockMvcRequestBuilders.post("/user")//perform模拟一个http请求,这里是get方法
                    .content(mapper.writeValueAsString(user))//用IoC建立一个User对象
                    .contentType(MediaType.APPLICATION_JSON_VALUE))//添加json类数据,转化为入参
            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200
            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出
            .andExpect(MockMvcResultMatchers.jsonPath("uname").value("wy"))//获取返回的json并核对对应的值是否一样
            .andReturn();//将结果返回出来
}
文章来自个人专栏
Java开发者绕不开的技术
12 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0