SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤

SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤

作者:一一哥

上一章节中,我带大家学习了Java中关于Test的详细内容,接下来在本节中,我带大家结合SpringBoot,再详细的学习一下测试的具体实现。

本文会从以下4个层面讲解SpringBoot中的测试功能实现

  • Service层单元测试;
  • Controller层单元测试;
  • 断言assertThat的使用;
  • 单元测试的事务回滚。

一.SpringBoot整合测试依赖的详细实现

1. 创建web项目

我们按照之前的经验,创建一个web程序,并将之改造成Spring Boot项目,具体过程略。
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第1张图片

2. 添加依赖包


    org.springframework.boot
    spring-boot-starter-data-jpa



    mysql
    mysql-connector-java



    org.springframework.boot
    spring-boot-starter-test
   test

3. 创建application.yml配置文件

server:
  port: 8080
spring:
  application:
    name: spring-test
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: syc
    url: jdbc:mysql://localhost:3306/spring-security?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false&serverTimezone=UTC
  jpa:
    database: mysql
    show-sql: true #开发阶段,打印要执行的sql语句.
    hibernate:
      ddl-auto: update

4. 创建User实体类

package com.yyg.boot.entity;

import lombok.Data;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Date;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/29
 * @Description Description
 */
@Entity
@Table(name="user")
@Data
@ToString
public class User implements Serializable {

    @Id
    @GeneratedValue(generator = "idGenerator",strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

}

5. 创建UserRepository实体仓库

package com.yyg.boot.repository;

import com.yyg.boot.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/29
 * @Description Description
 */
public interface UserRepository extends JpaRepository {

    User findByUsername(String username);

}

6. 定义Service层代码

创建UserService接口

package com.yyg.boot.service;

import com.yyg.boot.entity.User;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/29
 * @Description Description
 */
public interface UserService {

    /**
     * save user
     */
    User saveUser(User user);

    /**
     * find user by account
     */
    User findByUsername(String username);

}

定义UserServiceImpl实现类

package com.yyg.boot.service.impl;

import com.yyg.boot.entity.User;
import com.yyg.boot.repository.UserRepository;
import com.yyg.boot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/29
 * @Description Description
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User saveUser(User user) {
        return userRepository.save(user);
    }

    @Override
    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }

}

7. 创建Controller接口

package com.yyg.boot.web;

import com.yyg.boot.entity.User;
import com.yyg.boot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/29
 * @Description Description
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * save user
     */
    @PostMapping(value = "/save")
    public User save(@RequestBody User user) {
        return userService.saveUser(user);
    }

    /**
     * find user by username
     */
    @GetMapping(value = "/{username}")
    public User findUser(@PathVariable String username) {
        return userService.findByUsername(username);
    }

}

8. 创建入口类

package com.yyg.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/28
 * @Description Description
 */
@SpringBootApplication
public class SpringTestApplication {

    public static void main(String[] args){
        SpringApplication.run(SpringTestApplication.class,args);
    }

}

9. 创建Service层的测试类

9.1 自动创建测试类

Spring Boot中的单元测试类一般都要写在src/test/java目录下,并且测试类路径应该与要测试的类路径一直。

我们可以手动创建具的体测试类,但是在IDEA这样比较智能的开发工具中,我们其实可以通过IDEA工具自动创建测试类。

如下图所示:

也可以通过快捷键⇧⌘T(MAC)或者Ctrl+Shift+T(Window)来创建。

我们只需要把光标定位在要测试的类中任意位置,然后通过点击Navigate-->Test按钮,或者通过快捷键,IDEA就会自动帮我们在测试目录下创建出一个对应的测试类。

如下图所示:
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第2张图片

可以勾选要进行测试的方法,也可以自动产生@Before与@After等测试方法。


SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第3张图片

9.2 测试类内容

默认情况下,会产生如下所示的测试类:

package com.yyg.boot.service.impl;

import org.junit.Assert;
import org.junit.jupiter.api.Test;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/5/7
 * @Description Description
 */
class UserServiceImplTest {

    @Test
    void saveUser() {
    
    }
    
    @Test
    void findByUsername() {
        
    }

}

我们只需要在该测试类上添加@RunWith(SpringRunner.class)和@SpringBootTest注解就可以了。

9.3 UserServiceImplTest的具体实现

package com.yyg.boot.service.impl;

import com.yyg.boot.entity.User;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import static org.hamcrest.CoreMatchers.*;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/5/7
 * @Description Description
 */
@RunWith(SpringRunner.class)
@SpringBootTest
//注意:当添加@Transactional事务注解之后,对数据库的增/删/改操作数据,不会被真正存储到数据库中,因为事务没有被提交,可以防止测试数据污染真实数据.
//@Transactional
//@Rollback(true)
class UserServiceImplTest {

    @Autowired
    private UserServiceImpl userService;

    //利用@Rollback注解,对数据库进行增删改时是否进行回滚,默认是执行回滚操作.
    //@Rollback(true)
    @Test
    void saveUser() {
        User user=new User();
        user.setUsername("test2");
        user.setPassword("123");
        User result = userService.saveUser(user);
        Assert.assertThat(result,notNullValue());
    }

    @Test
    void findByUsername() {
        User user = userService.findByUsername("tom");
        Assert.assertThat(user.getUsername(),is(equalTo("tom")));
    }

}

9.4 service方法具体测试

我们只需要把光标定位在要测试的方法内部,然后右键选择run执行就可以了。
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第4张图片

一开始数据库中的数据如下:

测试成功的效果:
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第5张图片

此时数据库中的数据如下所示:
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第6张图片

查询方法的测试效果如下:
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第7张图片

当我们测试失败时的效果如下所示:
SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第8张图片

10. 进行Controller层的测试

10.1 自动创建出Controller层测试类

10.2 构建出MockMvc对象

package com.yyg.boot.web;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/5/7
 * @Description Description
 */
@RunWith(SpringRunner.class)
@SpringBootTest
//自动配置mock
@AutoConfigureMockMvc
class UserControllerTest {

    //创建出一个模拟的MVC对象
    @Autowired
    private MockMvc mvc;
    
    @Test
    public void save() throws Exception {
       
    }

    @Test
    public void findUser() throws Exception {
        
    }

}

10.3 具体测试代码实现

package com.yyg.boot.web;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

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

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/5/7
 * @Description Description
 */
@RunWith(SpringRunner.class)
@SpringBootTest
//配置mock
@AutoConfigureMockMvc
class UserControllerTest {

    //模拟一个MVC对象
    @Autowired
    private MockMvc mvc;

    @Test
    public void save() throws Exception {
        String json = "{\"username\":\"test3\",\"password\":\"123\"}";
        mvc.perform(MockMvcRequestBuilders.post("/user/save")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                //传json参数
                .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }

    @Test
    public void findUser() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/user/test2")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("test2"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.password").value("123"))
                .andDo(MockMvcResultHandlers.print());
    }

}

10.4 MockMvc执行解释

  • mockMvc.perform执行一个请求;

  • MockMvcRequestBuilders.get(“/user/save”)构造一个请求,Post请求就用.post方法;

  • contentType(MediaType.APPLICATION_JSON)代表发送端发送的数据格式是application/json;

  • accept(MediaType.APPLICATION_JSON)代表客户端希望接受的数据类型为application/json;

  • ResultActions.andExpect添加执行完成后的断言;

  • ResultActions.andExpect(MockMvcResultMatchers.status().isOk())方法看请求的状态响应码是否为200如果不是则抛异常,测试不通过;

  • andExpect(MockMvcResultMatchers.jsonPath("$.username").value("test2"))这里jsonPath用来获取username字段对比是否为”test2“,不是则测试不通过;

  • ResultActions.andDo添加一个结果处理器,表示要对结果做什么事情,比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。

11. 完整项目结构:

SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第9张图片

SpringBoot2.x系列教程77--SpringBoot中整合测试详细实现步骤_第10张图片

二. 断言assertThat的使用

1. assertThat断言简介

从JUnit4.4版本开始,结合 Hamcrest,提供了一个全新的断言语法——assertThat。

我们可以只使用一个assertThat断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。

2. assertThat 的基本语法:

assertThat( [value], [matcher statement] );

#value 是接下来想要测试的变量值;

#matcher statement 是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,
#如果 value 值与 matcher statement 所表达的期望值相符,则测试成功,否则测试失败。

3. assertThat 的优点

  • 优点 1:以前 JUnit 提供了很多的 assertion 语句,如:assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull 等,现在有了assertThat,即可以替代所有的 assertion 语句,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。

  • 优点 2:assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想要设定满足的条件,具有很强的易读性,而且使用起来更加灵活。

  • 优点 3:assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。

  • 优点 4:可以将这些 Matcher 匹配符联合起来灵活使用,达到更多目的。

4. 断言中的Matcher匹配符

// 想判断某个字符串 s 是否含有子字符串 "developer" 或 "Works" 中间的一个
// JUnit 4.4 以前的版本:assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works"))); 
// 匹配符 anyOf 表示任何一个条件满足则成立,类似于逻辑或 "||", 匹配符 containsString 表示是否含有参数子字符串。

字符相关匹配符
/**equalTo匹配符断言被测的testedValue等于expectedValue,
* equalTo可以断言数值之间,字符串之间和对象之间是否相等,相当于Object的equals方法
*/
assertThat(testedValue, equalTo(expectedValue));
/**equalToIgnoringCase匹配符断言被测的字符串testedString
*在忽略大小写的情况下等于expectedString
*/
assertThat(testedString, equalToIgnoringCase(expectedString));
/**equalToIgnoringWhiteSpace匹配符断言被测的字符串testedString
*在忽略头尾的任意个空格的情况下等于expectedString,
*注意:字符串中的空格不能被忽略
*/
assertThat(testedString, equalToIgnoringWhiteSpace(expectedString);
/**containsString匹配符断言被测的字符串testedString包含子字符串subString**/
assertThat(testedString, containsString(subString) );
/**endsWith匹配符断言被测的字符串testedString以子字符串suffix结尾*/
assertThat(testedString, endsWith(suffix));
/**startsWith匹配符断言被测的字符串testedString以子字符串prefix开始*/
assertThat(testedString, startsWith(prefix));
一般匹配符
/**nullValue()匹配符断言被测object的值为null*/
assertThat(object,nullValue());
/**notNullValue()匹配符断言被测object的值不为null*/
assertThat(object,notNullValue());
/**is匹配符断言被测的object等于后面给出匹配表达式*/
assertThat(testedString, is(equalTo(expectedValue)));
/**is匹配符简写应用之一,is(equalTo(x))的简写,断言testedValue等于expectedValue*/
assertThat(testedValue, is(expectedValue));
/**is匹配符简写应用之二,is(instanceOf(SomeClass.class))的简写,
*断言testedObject为Cheddar的实例
*/
assertThat(testedObject, is(Cheddar.class));
/**not匹配符和is匹配符正好相反,断言被测的object不等于后面给出的object*/
assertThat(testedString, not(expectedString));
/**allOf匹配符断言符合所有条件,相当于“与”(&&)*/
assertThat(testedNumber, allOf( greaterThan(8), lessThan(16) ) );
/**anyOf匹配符断言符合条件之一,相当于“或”(||)*/
assertThat(testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
数值相关匹配符
/**closeTo匹配符断言被测的浮点型数testedDouble在20.0¡À0.5范围之内*/
assertThat(testedDouble, closeTo( 20.0, 0.5 ));
/**greaterThan匹配符断言被测的数值testedNumber大于16.0*/
assertThat(testedNumber, greaterThan(16.0));
/** lessThan匹配符断言被测的数值testedNumber小于16.0*/
assertThat(testedNumber, lessThan (16.0));
/** greaterThanOrEqualTo匹配符断言被测的数值testedNumber大于等于16.0*/
assertThat(testedNumber, greaterThanOrEqualTo (16.0));
/** lessThanOrEqualTo匹配符断言被测的testedNumber小于等于16.0*/
assertThat(testedNumber, lessThanOrEqualTo (16.0));
集合相关匹配符
/**hasEntry匹配符断言被测的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项*/
assertThat(mapObject, hasEntry("key", "value" ) );
/**hasItem匹配符表明被测的迭代对象iterableObject含有元素element项则测试通过*/
assertThat(iterableObject, hasItem (element));
/** hasKey匹配符断言被测的Map对象mapObject含有键值“key”*/
assertThat(mapObject, hasKey ("key"));
/** hasValue匹配符断言被测的Map对象mapObject含有元素值value*/
assertThat(mapObject, hasValue(value));

三. 单元测试的事务回滚

我们在进行单元测试的时候,如果不想让测试数据对正式数据库,造成垃圾数据,可以开启事务功能。只需要在方法或者类上面添加@Transactional注解即可。

@RunWith(SpringRunner.class)
@SpringBootTest
//注意:当添加@Transactional事务注解之后,对数据库的增/删/改操作数据,不会被真正存储到数据库中,因为事务没有被提交,可以防止测试数据污染真实数据.
@Transactional
//@Rollback(true)
class UserServiceImplTest {

    @Autowired
    private UserServiceImpl userService;

    //利用@Rollback注解,对数据库进行增删改时是否进行回滚,默认是执行回滚操作.
    //@Transactional
    //@Rollback(true)
    @Test
    void saveUser() {
        User user=new User();
        user.setUsername("test2");
        user.setPassword("123");
        User result = userService.saveUser(user);
        Assert.assertThat(result,notNullValue());
    }

    @Test
    void findByUsername() {
        User user = userService.findByUsername("tom");
        Assert.assertThat(user.getUsername(),is(equalTo("tom")));
    }

}

这样测试完数据就会回滚了,不会造成垃圾数据。如果你想关闭事务的回滚功能,要么不添加@Transactional注解,要么可以加上@Rollback(false)注解。

@Rollback表示事务执行完回滚,支持传入一个参数value,默认true即回滚,false不回滚。

但是如果你使用的数据库是Mysql,有时候会发现加了注解@Transactional 也不会回滚,那么你就要查看一下你的默认引擎是不是InnoDB,如果不是就要改成InnoDB。

 

你可能感兴趣的:(Spring,Boot,2,测试)