springboot+junit5 练习,由浅入深(一)

系列文章目录

[第一章 springboot+junit5 练习,由浅入深(一)](https://editor.csdn.net/md?not_checkout=1&articleId=120584779)

文章目录


前言

一直以来,我的开发模式都是:需求分析——>code实现——>编写单元测试,通过对测试驱动开发 (TDD)的概念理解,我决定尝试新的开发模式:需求分析——>设计单元测试——>code功能实现。

一、需求分析

首先,我参考了一个简单的留言版网站后端需求,如下:

  • 用户可以在网站上注册

    • 需要填写 username, password, email。
    • username需要检查:不可为空,只能使用字母和数字,长度在5~20之间,不能与已有用户名重复
    • password需要检查:不可为空,长度在8~20之间,至少包含一个大写、一个小写、一个数字、一个特殊符号
    • email需要检查:不可为空,格式要正确,不能与已有email重复。为简单起见,不需要发送邮件确认
  • 用户可以在网站上登录

    • 使用username+password,或者email+password 登录
    • 提供”remember me”功能,登录后一个月内不需要重新登录
    • 如果未勾选”remember me”,则关闭浏览器后再次访问会提示注册或登录
    • 用户登录后,可以获取用户信息
  • 用户登录后,可以发表留言

    • 留言长度在3~200字之间,可以为中文
    • 会记录留言发表时间
  • 技术要求

    • 后端提供的Restful API

二、搭建系统

为了简单方便,我打算使用SpringBoot + Sqlite ,开发工具用IDEA

1. 搭建框架

IDEA搭建SpringBoot非常简单,网上一搜一大把,我就不重复造*了,这里贴张目录结构图:
springboot+junit5 练习,由浅入深(一)

  • Maven配置:
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <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>

        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.31</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.13</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>

这里特别说明下为什么要排除org.junit.vintage项,因为本次练习的Junit5,使用的是junit-jupiter-engine,org.junit.vintage(junit-vintage-engine)是Junit4的使用的引擎,若你在Junit5中使用了junit-vintage-engine会报错。

2. 设计Controller和Service

根据需求,创建用户模块REST接口UserController和用户模块服务UserService,该模块提供注册,登录,获取当前登录用户信息的简单功能。代码设计如下:

  • Controller:
import com.practice.comments.dto.LoginDTO;
import com.practice.comments.dto.RegisterDTO;
import com.practice.comments.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class UserController {
  @Autowired
  UserService userService;

  @PostMapping("/register")
  public String register(@RequestBody RegisterDTO registerDTO){
    return userService.register(registerDTO);
  }

  @PostMapping("/logon")
  public String login(@RequestBody LoginDTO loginDTO){
    return userService.login(loginDTO);
  }

  @GetMapping("/userInfo")
  public String getUserInfo(){
    return userService.getUserInfo();
  }
}
  • Service:
import com.practice.comments.dto.LoginDTO;
import com.practice.comments.dto.RegisterDTO;
import org.springframework.stereotype.Service;

@Service
public interface UserService {

  String register(RegisterDTO registerDTO);

  String login(LoginDTO loginDTO);

  String getUserInfo();
}
  • DTO:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class loginDTO {
  private String userName;
  private String userPassword;
  private boolean rememberMe;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterDTO {
  private String userName;
  private String userPassword;
  private String email;
}

三、设计单元测试

好了,在实现具体的功能前,可以先根据现有的框架和接口,来设计初步的单元测试。在IDEA中,可以快速生成类的测试代码,如下图,点击右键——选择Go To——Test:

springboot+junit5 练习,由浅入深(一)

然后输入测试类名,默认是测试的类名后面加Test,如下图,打勾的也可以后面加上
springboot+junit5 练习,由浅入深(一)

生成测试类如下:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class UserControllerTest {

  @BeforeEach
  void setUp() {//每个测试方法调用前,都会先调用该方法
  }

  @AfterEach
  void tearDown() {//每个测试方法调用后,都会再调用该方法
  }

  @Test
  void register() {
  }

  @Test
  void login() {
  }

  @Test
  void getUserInfo() {
  }
}

先设计注册功能的测试,注册需求如下:

  • 用户可以在网站上注册

  • 需要填写 username, password, email。

    • username需要检查:不可为空,只能使用字母和数字,长度在5~20之间,不能与已有用户名重复
    • password需要检查:不可为空,长度在8~20之间,至少包含一个大写、一个小写、一个数字、一个特殊符号
    • email需要检查:不可为空,格式要正确,不能与已有email重复。为简单起见,不需要发送邮件确认

填充的测试代码如下:

import com.practice.comments.service.UserService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpMethod;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@WebMvcTest(controllers = UserController.class)
class UserControllerTest {
  private static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json;charset=UTF-8";
  @Autowired
  MockMvc mockMvc;

  @Test
  void register() throws Exception {
    String url = "/register";
    String contentJson = "{\"userName\":\"test1\",\"userPassword\":\"123abc\",\"email\":\"123@345.com\"}";
    String resultJson="{\"status\":\"0\",\"content\":\"注册成功!\"}";
    mockMvc.perform(
      MockMvcRequestBuilders.request(HttpMethod.POST, url)
              // 设置返回值类型为json utf-8,否则默认为ISO-8859-1
              .accept(APPLICATION_JSON_CHARSET_UTF_8)
              .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson))
              .andExpect(MockMvcResultMatchers.status().isOk())
              .andExpect(MockMvcResultMatchers.content()
                      .string(containsString(resultJson)))
              .andDo(print());
  }
}

这里解释一下使用@WebMvcTest注解,该注解是可以只针对参数中的controller进行实例化,而不用启动整个SpringBoot,很适合在没有实现具体功能时进行测试。

再介绍下MockMvcmock是比较常用的测试框架,主要作用是对对象进行模拟,而MockMvc是专门针对Http交互进行模拟,上面代码中就是通过mockMvc.perform()来模拟发起http请求。

然后执行register()测试方法,会得到如下报错信息:

java.lang.IllegalStateException: Failed to load ApplicationContext
……No qualifying bean of type 'com.practice.comments.service.UserService' available

报错的意思是没有符合UserService的实现类,原因很明显,我根本没有实现UserService的功能,也没有在当前测试类中进行实例化。所以还需要通过@mockbean模拟一个UserService。
在UserControllerTest中加入以下代码

  @MockBean //模拟一个springBean
  UserService userService;

然后再次执行register()测试方法,会得到如下报错信息:

java.lang.AssertionError: Response content
Expected: a string containing "{\"status\":\"0\",\"content\":\"注册成功!\"}"
     but: was ""
Expected :a string containing "{\"status\":\"0\",\"content\":\"注册成功!\"}"
Actual   :""

该报错是断言不匹配,即没有返回我期望的结果,原因是因为UserService是模拟的,若不模拟返回结果,则默认返回空或空字符串或0。所以需要提前模拟userService的返回值:

修改register()方法如下:

@Test
  void register() throws Exception {
    String url = "/register";
    String contentJson = "{\"userName\":\"test1\",\"userPassword\":\"123abc\",\"email\":\"123@345.com\"}";
    String resultJson="{\"status\":\"0\",\"content\":\"注册成功!\"}";
    //模拟userService的返回值。any()表示是任意参数
    when(userService.register(any())).thenReturn(resultJson);
    
    mockMvc.perform(
      MockMvcRequestBuilders.request(HttpMethod.POST, url)
              // 设置返回值类型为json utf-8,否则默认为ISO-8859-1
              .accept(APPLICATION_JSON_CHARSET_UTF_8)
              .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson))
              .andExpect(MockMvcResultMatchers.status().isOk())
              .andExpect(MockMvcResultMatchers.content()
                      .string(containsString(resultJson)))
              .andDo(print());
  }

这样,任何时候调用userService.register方法,都会直接返回"{\"status\":\"0\",\"content\":\"注册成功!\"}"

再次执行register(),测试通过了。

四、完善测试

上一节我简单了设计了注册成功的测试,接下来,需要根据需求,设计失败分支的测试,
代码修改如下:

  @DisplayName("通过cvs提供测试数据,参数化测试注册功能")
  @ParameterizedTest
  @CsvFileSource(resources = "/Wrong_Register.csv")
  void register(String  contentJson, String  resultJson) throws Exception {
    String url = "/register";
    mockPerform(url, contentJson, resultJson);
  }

  private void mockPerform(String url, String contentJson, String resultJson) throws Exception {
    mockMvc.perform(
            MockMvcRequestBuilders.request(HttpMethod.POST, url)
                    // 设置返回值类型为json utf-8,否则默认为ISO-8859-1
                    .accept(APPLICATION_JSON_CHARSET_UTF_8)
                    .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content()
                    .string(containsString(resultJson)))
            .andDo(print());
  }

这里用到了Junit5的新特性:“参数化测试”,该特性可以让我们运行单个测试多次,且使得每次运行仅仅是参数不同而已。
需要在测试方法添加参数,把@Test注解替换成:@ParameterizedTest@CsvFileSource(resources = "外部文件相对路径")

在resource下新建csv文档,用Excel打开,按参数顺序进行列填充,每行一组测试:
springboot+junit5 练习,由浅入深(一)

  • csv
    springboot+junit5 练习,由浅入深(一)
    这样,我们用户注册功能的测试就设计完成了,后续只要实现具体的功能,就可以利用这个测试类反复进行测试了(记得将模拟的sprinbBean为替换实现的哦~)。

总结

本章我们通过SpringBoot和Junit5,展示了分层测试和参数化测试的功能特性,使得代码优雅效率。下一章,我们将结合其他场景,介绍不同的测试方法。

上一篇:部署 harbor 私有仓库时无法正常启动 harbor 实例及 client 端无法正常访问用户界面和 register 服务


下一篇:GatewayWorker使用方式及工作原理