Spring单测实现原理

Spock的单测代码是继承自Specification基类,而Specification又是基于Junit的注解Testable实现的

image-20210723113620973

与早期版本,基于Junit4的注解@RunWith()实现,当前版本实现的@Testable注解,是Junit5平台式的注解,Junit5向下兼容Junit4,因此在测试时,Junit4的依然可以使用

PowerMock的PowerMockRunner也是继承自Junit,所以使用PowerMock的@PowerMockRunnerDelegate()注解可以指定Spock的父类Sputnik去代理运行Power Mock,这样就可以在Spock里使用PowerMock去模拟静态方法、final方法、私有方法等

其实Spock自带的GroovyMock可以对groovy文件的静态方法mock,但对Java代码的支持不完整,只能mock当前Java类的静态方法,官方给出的解释:https://spockframework.org/spock/docs/2.0/all_in_one.html#_mocking_static_methods

image-20210723114035033

因为我们项目中存在很多调用静态方法的代码,现阶段考虑重构业务代码的成本过高,所以这里使用扩展Power Mock的方式测试静态方法

注意事项:

演示的示例中Spock使用的是2.0的版本,在Spock 2.x 的版本里官方团队已经移除Sputnik,不再支持代理运行power mock的方式

因为Spock 2.0是基于JUnit5,我们项目以前的单元测试代码都是基于Junit4编写的,换成Junit5后,需要修改现有的java单测,比如指定代理运行,使用power mock的地方要换成Junit5的扩展语法。

为了演示将Spock的版本降低为:1.3-groovy-2.5,若降低了Spock的版本,则Groovy的版本也需要降低至2.X.X的版本,否则会不兼容

pom引入powermock依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- power mock -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>

注意版本号的问题,由于高版本会废弃早期的用法,又会支持高端的用法,所以如果更换了高的版本号,可能会造成一些包或方法不支持,则不需要查询新的支持的用法。

Spock代理Power Mock

首先看下需要测试的业务代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 通过userId获取用户信息
*
* @param uid 用户id
* @return 返回用户信息
*/
public UserVO getUserByIdStatic(int uid) {
List<UserDTO> users = userDAO.getUserInfo();
UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null);
UserVO userVO = new UserVO();
if (null == userDTO) {
return userVO;
}
userVO.setId(userDTO.getId());
userVO.setName(userDTO.getName());
userVO.setSex(userDTO.getSex());
userVO.setAge(userDTO.getAge());
// 显示邮编
if ("上海".equals(userDTO.getProvince())) {
userVO.setAbbreviation("沪");
userVO.setPostCode(200000);
}
if ("北京".equals(userDTO.getProvince())) {
userVO.setAbbreviation("京");
userVO.setPostCode(100000);
}
// 手机号处理
if (null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())) {
userVO.setTelephone(userDTO.getTelephone().substring(0, 3) + "****" + userDTO.getTelephone().substring(7));
}
// 静态方法调用,身份证工具类
Map<String, String> idMap = IDNumberUtils.getBirAgeSex(userDTO.getIdNo());
userVO.setAge(idMap.get("age") != null ? Integer.parseInt(idMap.get("age")) : 0);
// 静态方法调用记录日志
LogUtils.info("response user", userVO.toString());
return userVO;
}

在倒数第4行和倒数第2行代码分别调用了 “身份证工具类IDNumberUtils.getBirAgeSex()” 和 “LogUtils.info()” 日志记录的方法,如果要对这两个静态方法进行mock,我们可以使用Spock+Power Mock的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.spock.example

import com.spock.example.dao.UserDAO
import com.spock.example.dto.UserDTO
import com.spock.example.service.UserService
import com.spock.example.utils.IDNumberUtils
import com.spock.example.utils.LogUtils
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import spock.lang.Specification

/**
* @author jinglv* @date 2021/7/28 4:24 下午
*/
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([LogUtils.class, IDNumberUtils.class])
@SuppressStaticInitializationFor(["com.spock.example.utils.LogUtils"])
class UserServiceStaticTest extends Specification {

def processor = new UserService()
def dao = Mock(UserDAO)

void setup() {
processor.userDAO = dao
// mock静态类
PowerMockito.mockStatic(LogUtils.class)
PowerMockito.mockStatic(IDNumberUtils.class)
}

def "GetUserByIdStatic"() {
given: "设置请求参数"
def user1 = new UserDTO(id: 1, name: "张三", province: "上海")
def user2 = new UserDTO(id: 2, name: "李四", province: "江苏")
def idMap = ["birthday": "1992-09-18", "sex": "男", "age": "28"]

and: "mock掉接口返回的用户信息"
dao.getUserInfo() >> [user1, user2]

and: "mock静态方法返回值"
PowerMockito.when(IDNumberUtils.getBirAgeSex(Mockito.any())).thenReturn(idMap)

when: "调用获取用户信息方法"
def response = processor.getUserByIdStatic(1)

then: "验证返回结果是否符合预期值"
with(response) {
name == "张三"
abbreviation == "沪"
postCode == 200000
age == 28
}
}
}

代码解说:

  • 在UserServiceStaticTest类的头部使用@PowerMockRunnerDelegate(Sputnik.class)注解,交给Spock代理执行,这样既可以使用 Spock + groovy 的各种功能,又可以使用power mock的对静态,final等方法的mock
  • @SuppressStaticInitializationFor(["com.spock.example.utils.LogUtils"])这行代码的作用是限制LogUtils类里的静态代码块初始化,因为LogUtils类在第一次调用时会加载一些本地资源配置,比较耗费时间,所以可以使用power mock禁止初始化,然后在setup()方法里对两个静态类进行mock设置,PowerMockito.mockStatic(LogUtils.class),PowerMockito.mockStatic(IDNumberUtils.class),最后在GetUserByIdStatic测试方法里对getBirAgeSex()方法指定返回默认值:PowerMockito.when(IDNumberUtils.getBirAgeSex(Mockito.any())).thenReturn(idMap)

运行结果

image-20210728164900305

从上面看到,控制台输出Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST,这是powermock警告信息,不影响运行结果。

学习大佬文章:https://javakk.com/302.html