Spring Boot 基于 Mockito 单元测试

目录

前言

依赖与配置

pom

yml

演示代码

UserEntity

UserDao

UserService

EncryptUtil

单元测试

SpringBootTest

Mockito

PowerMock


前言

在网上刷到过"水货程序员"相关的帖子,列举了一些水货程序员的特征,其中一条就是不写单元测试,或者不知道单元测试是啥。看得瑟瑟发抖,完全不敢说话。

在小公司里当开发,对单元测试根本没有要求,测试也就是本地启动服务,自己调下接口看看是否调通,以及和前端本地联调。毕业后入行以来都没写过,想写也不知道该怎么做。自己想摆脱"水货程序员"标签去写单元测试,也只是照着网上博客,本地写一写,不知道写得是否规范,所以从没提交过单元测试代码。

后面跳槽,项目有要求写单元测试了,这就有能够参考的单元测试代码了。故记录下如何在Spring Boot 项目中写业务代码的单元测试代码。

依赖与配置

pom

XML 复制代码
    <properties>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>   

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    <dependencies>

yml

server:

port: 8888

spring:

datasource:

jdbc-url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true

username: root

password: root

driver-class-name: com.mysql.cj.jdbc.Driver

jpa:

show-sql: true

properties:

hibernate:

hbm2ddl:

auto: update

dialect: org.hibernate.dialect.MySQL5InnoDBDialect

演示代码

UserEntity

java 复制代码
@Entity
@Table ( name = "user")
public class UserEntity {
    private Integer id;
    private String userName;
    private String password;


    @Id
    @GeneratedValue ( strategy = GenerationType.IDENTITY)
    @Column ( name = "id" )
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Basic
    @Column ( name = "username" )
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    @Basic
    @Column ( name = "password" )
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

UserDao

java 复制代码
@Repository
public interface UserDao extends JpaRepository<UserEntity, Integer> {

    boolean existByUserName(String userName);

}

UserService

简单的一个注册用户方法,若存在同名用户则抛出异常,否则加密密码,然后入库。

java 复制代码
@Service
public class UserService {

    UserDao userDao;

    public Integer register(String userName, String password) {

        if (userDao.existByUserName(userName)) {
            throw new RuntimeException(String.format("当前用户名【%s】已经注册", userName));
        }

        UserEntity entity = new UserEntity();
        entity.setUserName(userName);
        password = EncryptUtil.encrypt(password);
        entity.setPassword(password);

        entity = userDao.save(entity);

        return entity.getId();
    }
}

EncryptUtil

加密工具类,只做演示,故简单返回一个字符串。

java 复制代码
public class EncryptUtil {

    public static String encrypt(String source) {
        return String.format("%s-encrypt", source);
    }

    public static String decrypt(String source) {
        return String.format("%s-decrypt", source);
    }
}

单元测试

这里使用的 Junit 框架是 Junit4,测试类引用的 @Test 注解引用自org.junit.Test包下的。

与 Junit5 有所不同,Junit5 中的 @Test 是引用自 org.junit.jupiter.api.Test

这里代码使用的 Spring Boot 版本为 2.3.7,相对较低,还集成了 junit,所以就使用 Junit4 来写单元测试。在更新的版本里就没有集成 junit,而是集成了 junit-jupiter。

如果使用了更高版本的 Spring Boot,下面代码的部分注解是没有的,比如 @RunWith

所以写单元测试代码还得根据对应的 Spring Boot 版本而定,不同版本注解的使用略有不同

SpringBootTest

不使用Mockito框架,依赖注入要测试的业务Service对象,然后直接调用测试的方法。这种写法在跑单元测试时就必须启动 Spring 容器,这样才能够注入依赖,否则单元测试中拿到的对象就会为null,运行中空指针异常,导致单元测试失败。

这种写法会出现一个很无语的场景,跑一个单元测试耗时0.1秒,但是启动Spring容器却要好几秒,如果项目很大,那么简单跑一个单元测试,绝大部分时间都花在启动容器上了。而且都启动容器了,那还不如本地直接启动服务用postman去调用接口方便。所以基本上没有这么去写单元测试。

java 复制代码
//如果 @Test 引用自 org.junit.jupiter.api.Test
//则不需要 @RunWith 注解
@RunWith (SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    public void registerTest() {
        String userName = "abc";
        String password = "123456";

        int id = userService.register(userName, password);

        Assert.assertNotNull("id为空", id);
    }

}

Mockito

Mockito 两个注解 @InjectMocks @Mock 使用区别如下:

  • 这里测试的是 UserService,就用 @InjectMocks 注解注入 UserService;
  • 被测试的类(UserService)中通过 @Autowired 注解注入的依赖,在测试类里面就用@Mock注解创建实例。UserService 依赖了 UserDao,故使用@Mock注解来注入UserDao;

让Mockito的注解生效,则需要在测试类上使用@RunWith注解,注解中value的值为 Runner,Runner其实就是各个框架在跑测试case的前后处理一些逻辑。mockito的MockitoJUnitRunner,作用就是在跑单测之前,将@Mock注解的对象构造出来。

这样就可以使用 mock 对象来写单元测试了,先定义 mock 对象的行为,然后调用被测试类的方法,最后验证返回结果是否符合预期。

严谨一点的话,可以将代码中的逻辑分支都写对应的单元测试。

java 复制代码
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @InjectMocks
    UserService userService;

    @Mock
    UserDao userDao;

    @Test
    public void registerSuccessTest() {
        String userName = "abc";
        String password = "123456";

        UserEntity user = new UserEntity();
        user.setId(1);
        user.setUserName(userName);
        user.setPassword(password);

        Mockito.when(userDao.existByUserName(userName)).thenReturn(false);
        Mockito.when(userDao.save(any())).thenReturn(user);

        Integer id = userService.register(userName, password);

        Assert.assertEquals("注册失败", 1, id.intValue());

        Assert.assertNotNull("id为空", id);
    }

    @Test
    public void registerFailTest() {
        String userName = "abc";
        String password = "123456";
        Mockito.when(userDao.existByUserName(any())).thenReturn(true);

        Assert.assertThrows(RuntimeException.class, () -> userService.register(userName, password));
    }
}

PowerMock

使用 Mockito 基本上就能写大部分单元测试了。但是在某些情况下可能无法满足特定的 Mock 需求,比如对static class, final class,constructor,private method等的mock操作。

在工作中,使用过数据脱敏的静态工具类,由于这个静态工具类注入了一个脱敏规则相关的对象。所以导致在单元测试中无法直接使用这个工具类,会报空指针错误。面对这种情况,就需要引入 PowerMock 框架来解决。

PowerMock 在 Mockito 的基础上扩展而来,支持 Mockito 的操作,也拓展了 static class, final class,constructor,private method等的mock操作。

pom 文件新增以下依赖

XML 复制代码
    <properties>
        <powermock.version>2.0.2</powermock.version>
    </properties>      
  
   <dependencies>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>${powermock.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>${powermock.version}</version>
            <scope>test</scope>
        </dependency>
   </dependencies>

单元测试代码

  • @RunWith(PowerMockRunner.class) 表示由 PowerMockRunner 去完成 mock 对象的创建
  • @PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) 然后委托给 SpringJUnit4ClassRunner 去做依赖注入以及执行单元测试代码
  • @PrepareForTest({EncryptUtil.class}) PowerMock 去 mock【static class, final class,constructor,private method】时,需要将静态类写在 @prepareForTest 注解里
java 复制代码
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
@PrepareForTest({EncryptUtil.class})
public class UserServiceTest {

    @InjectMocks
    UserService userService;

    @Mock
    UserDao userDao;

    @Test
    public void registerSuccessTest() {
        String userName = "abc";
        String password = "123456";

        UserEntity user = new UserEntity();
        user.setId(1);
        user.setUserName(userName);
        user.setPassword(password);

        PowerMockito.mockStatic(EncryptUtil.class);
        Mockito.when(EncryptUtil.encrypt(any())).thenReturn("password");

        Mockito.when(userDao.existByUserName(userName)).thenReturn(false);
        Mockito.when(userDao.save(any())).thenReturn(user);

        Integer id = userService.register(userName, password);

        Assert.assertEquals("注册失败", 1, id.intValue());
    }

    @Test
    public void registerFailTest() {
        String userName = "abc";
        String password = "123456";
        Mockito.when(userDao.existByUserName(any())).thenReturn(true);

        Assert.assertThrows(RuntimeException.class, () -> userService.register(userName, password));
    }
}
相关推荐
运维&陈同学15 分钟前
【Elasticsearch05】企业级日志分析系统ELK之集群工作原理
运维·开发语言·后端·python·elasticsearch·自动化·jenkins·哈希算法
ZHOUPUYU1 小时前
最新 neo4j 5.26版本下载安装配置步骤【附安装包】
java·后端·jdk·nosql·数据库开发·neo4j·图形数据库
Q_19284999062 小时前
基于Spring Boot的找律师系统
java·spring boot·后端
ZVAyIVqt0UFji3 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
SomeB1oody4 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
AI人H哥会Java6 小时前
【Spring】Spring的模块架构与生态圈—Spring MVC与Spring WebFlux
java·开发语言·后端·spring·架构
毕设资源大全6 小时前
基于SpringBoot+html+vue实现的林业产品推荐系统【源码+文档+数据库文件+包部署成功+答疑解惑问到会为止】
java·数据库·vue.js·spring boot·后端·mysql·html
Watermelon_Mr6 小时前
Spring(三)-SpringWeb-概述、特点、搭建、运行流程、组件、接受请求、获取请求数据、特殊处理、拦截器
java·后端·spring
唐墨1237 小时前
golang自定义MarshalJSON、UnmarshalJSON 原理和技巧
开发语言·后端·golang
凡人的AI工具箱7 小时前
每天40分玩转Django:Django测试
数据库·人工智能·后端·python·django·sqlite