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));
    }
}
相关推荐
咚为5 小时前
Rust Print 终极指南:从底层原理到全场景实战
开发语言·后端·rust
二哈喇子!5 小时前
SpringBoot项目右上角选择ProjectNameApplication的配置
java·spring boot
二哈喇子!5 小时前
基于Spring Boot框架的车库停车管理系统的设计与实现
java·spring boot·后端·计算机毕业设计
二哈喇子!5 小时前
基于SpringBoot框架的水之森海底世界游玩系统
spring boot·旅游
二哈喇子!5 小时前
Java框架精品项目【用于个人学习】
java·spring boot·学习
二哈喇子!6 小时前
基于SpringBoot框架的网上购书系统的设计与实现
java·大数据·spring boot
二哈喇子!6 小时前
基于JavaSE的淘宝卖鞋后端管理系统的设计与实现
java·spring boot·spring
Coder_Boy_6 小时前
基于SpringAI的在线考试系统-智能考试系统-学习分析模块
java·开发语言·数据库·spring boot·ddd·tdd
高山上有一只小老虎7 小时前
mybatisplus实现分页查询
java·spring boot·mybatis
Loo国昌8 小时前
【LangChain1.0】第九阶段:文档处理工程 (LlamaIndex)
人工智能·后端·python·算法·langchain