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));
    }
}
相关推荐
2301_32241428045 小时前
基于SpringBoot+Vue的休闲零食超市管理系统的设计与实现59004-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·vue.js·spring boot·redis·mysql·决策树·推荐算法
金銀銅鐵7 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路8 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x8 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn10 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈11 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805111 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234512 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒12 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记12 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端