Spring Boot 基于 Mockito 单元测试_spring boot mockito
目录
前言
依赖与配置
pom
yml
演示代码
UserEntity
UserDao
UserService
EncryptUtil
SpringBootTest
Mockito
PowerMock
前言
在网上刷到过“水货程序员”相关的帖子,列举了一些水货程序员的特征,其中一条就是不写单元测试,或者不知道单元测试是啥。看得瑟瑟发抖,完全不敢说话。
在小公司里当开发,对单元测试根本没有要求,测试也就是本地启动服务,自己调下接口看看是否调通,以及和前端本地联调。毕业后入行以来都没写过,想写也不知道该怎么做。自己想摆脱“水货程序员”标签去写单元测试,也只是照着网上博客,本地写一写,不知道写得是否规范,所以从没提交过单元测试代码。
后面跳槽,项目有要求写单元测试了,这就有能够参考的单元测试代码了。故记录下如何在Spring Boot 项目中写业务代码的单元测试代码。
依赖与配置
pom
2.3.7.RELEASE org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test
yml
server:
port: 8888spring:
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
@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
@Repositorypublic interface UserDao extends JpaRepository { boolean existByUserName(String userName);}
UserService
简单的一个注册用户方法,若存在同名用户则抛出异常,否则加密密码,然后入库。
@Servicepublic 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
加密工具类,只做演示,故简单返回一个字符串。
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去调用接口方便。所以基本上没有这么去写单元测试。
//如果 @Test 引用自 org.junit.jupiter.api.Test//则不需要 @RunWith 注解@RunWith (SpringRunner.class)@SpringBootTestpublic 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 对象的行为,然后调用被测试类的方法,最后验证返回结果是否符合预期。
严谨一点的话,可以将代码中的逻辑分支都写对应的单元测试。
@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 文件新增以下依赖
2.0.2 org.powermock powermock-module-junit4 ${powermock.version} test org.powermock powermock-api-mockito2 ${powermock.version} test
单元测试代码
- @RunWith(PowerMockRunner.class) 表示由 PowerMockRunner 去完成 mock 对象的创建
- @PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) 然后委托给 SpringJUnit4ClassRunner 去做依赖注入以及执行单元测试代码
- @PrepareForTest({EncryptUtil.class}) PowerMock 去 mock【static class, final class,constructor,private method】时,需要将静态类写在 @prepareForTest 注解里
@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)); }}