吃透单元测试:Spock单元测试框架的应用与实践
Posted 吃透Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了吃透单元测试:Spock单元测试框架的应用与实践相关的知识,希望对你有一定的参考价值。
一,单元测试
单元测试是对软件基本组成单元进行的测试,如函数或一个类的方法。程序是由函数组成的,每个函数都要健壮,这样才能保证程序的整体质量。单元测试是对软件未来的一项必不可少的投资。”具体来说,单元测试有哪些收益呢?
- 它是最容易保证代码覆盖率达到100%的测试。
- 可以⼤幅降低上线时的紧张指数。
- 单元测试能更快地发现问题。
- 单元测试的性价比最高,因为错误发现的越晚,修复它的成本就越高,而且难度呈指数式增长,所以我们要尽早地进行测试
- 编码人员,一般也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序的人,其他任何人都无法做到这一点。
- 有助于源码的优化,使之更加规范,快速反馈,可以放心进行重构。
尽管单元测试有如此的收益,但在我们日常的工作中,仍然存在不少项目它们的单元测试要么是不完整要么是缺失的。
1,为什么人人都讨厌写单测
要额外写很多很多的代码
一个高覆盖率的单测代码,往往比你要测试的,真正开发的业务代码要多,甚至是业务代码的好几倍。这让人觉得难以接受,你想想开发 5 分钟,单测 2 小时是什么样的心情。而且并不是单测写完就没事了,后面业务要是变更了,你所写的单测代码也要同步维护。
时间成本
代码逻辑过于复杂,写单元测试时耗费的时间较长,任务重、工期紧,写一个单测的时间可以实现一个需求,那么你如何去选?显而易见。
写单测是一件很无趣的事情
因为他比较死,主要目的就是为了验证,相比之下他更像是个体力活,没有真正写业务代码那种创造的成就感。写出来,验证不出bug很失落,白写了,验证出bug又感到自己是在打自己脸。
2,为什么又必须写单测
所以得到的结论就是不写单测?那么问题又来了,出来混迟早是要还的,上线出了问题,最终责任人是谁?不是提需求的产品、不是没发现问题的测试同学,他们顶多就是连带责任。最该负责的肯定是写这段代码的你。
所以单元测试保护的不仅仅是程序,更保护的是写程序的你。 最后得出了一个无可奈何的结论,单测是个让人又爱又恨的东西,是不想做但又不得不做的事情。
虽然我们没办法改变要写单测这件事,但是我们可以改变怎么去写单元测试这件事。
既然我们不得不去写单元测试,那么今天就为大家推荐一款比较神奇的单元测试框架,Spock去提高你编写单测的效率。
二,Spock是什么
spock官网:https://spockframework.org/spock/docs/2.0/index.html
Spock是国外一款优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。
那么spock 是如何提高编写单测的效率呢?
- 它可以用更少的代码去实现单元测试,让你可以更加专注于去验证结果而不是写单测代码的过程。(Spock使用groovy动态语言的特点)
- **他有更好的语义化,让你的单测代码可读性更高。**Spock提供多种语义标签,如: given、when、then、expect、where、with、and 等,从行为上规范单测代码,每一种标签对应一种语义,让我们的单测代码结构具有层次感,功能模块划分清晰,便于后期维护。
三,Spock使用
Spock引入
<!--引入 groovy 依赖-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.15</version>
<scope>test</scope>
</dependency>
<!--引入spock 与 spring 集成包-->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.2-groovy-2.4</version>
<scope>test</scope>
</dependency>
Spock自带Mock功能,所以我们可以来Mock非静态方法。但是遇到静态方法时,我们需要导入powermock
<!--powermock -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
但是当我们需要测试dao层的sql语句时,我们可以结合H2内存数据库使用,此时需要引入:
<!--db unit-->
<dependency>
<groupId>com.github.janbols</groupId>
<artifactId>spock-dbunit</artifactId>
<version>0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
以上就能满足我们平时用Spock来写单测的日常功能。
下面通过一个简单的例子,来体验一下Spock和junit的不同:
public class IDNumberUtils
/**
* 根据身份证号码获取出生日期和年龄
*
* @param idNo 18位身份证号码
* @return 返回格式: birth: 1992-01-01 age: 29
*/
public static Map<String, String> getBirthAge(String idNo)
String birthday = "";
String age = "";
int year = Calendar.getInstance().get(Calendar.YEAR);
birthday = idNo.substring(6, 10) + "-" + idNo.substring(10, 12) + "-" + idNo.substring(12, 14);
age = String.valueOf(year - Integer.valueOf(idNo.substring(6, 10)));
Map<String, String> result = new HashMap<>();
result.put("birthday", birthday);
result.put("age",age);
return result;
Junit单测:
public class IDNumberUtilsJunitTest
@ParameterizedTest
@MethodSource("getBirthAgeParams")
public void testGetBirthAge(String idNo,Predicate<Map<String,String>> predicate)
Map<String, String> birthAgeMap = IDNumberUtils.getBirthAge(idNo);
Assertions.assertTrue(predicate.test(birthAgeMap));
public static Object[] getBirthAgeParams()
return new Object[]
new Object[]
"410225199208091234",(Predicate<Map<String,String>>) map -> "birthday=1992-08-09, age=29".equals(map.toString())
,
new Object[]
"410225199308091234",(Predicate<Map<String,String>>) map -> "birthday=1993-08-09, age=28".equals(map.toString())
,
new Object[]
"410225199408091234",(Predicate<Map<String,String>>) map -> "birthday=1994-08-09, age=27".equals(map.toString())
,
new Object[]
"410225199508091234",(Predicate<Map<String,String>>) map -> "birthday=1995-08-09, age=26".equals(map.toString())
,
new Object[]
"410225199608091234",(Predicate<Map<String,String>>) map -> "birthday=1996-08-09, age=25".equals(map.toString())
,
;
Spock单测:
class IDNumberUtilsGroovyTest extends Specification
@Unroll
def "身份证号:#idNO 的生日,年龄是:#result"()
expect: "执行以及结果验证"
IDNumberUtils.getBirthAge(idNO) == result
where: "测试用例覆盖"
idNO || result
"410225199208091234" || ["birthday": "1992-08-09", "age": "29"]
"410225199308091234" || ["birthday": "1993-08-09", "age": "28"]
"410225199408091234" || ["birthday": "1994-08-09", "age": "27"]
"410225199508091234" || ["birthday": "1995-08-09", "age": "26"]
"410225199608091234" || ["birthday": "1996-08-09", "age": "25"]
- def 是 groovy 的关键字,可以用来定义变量跟方法名。
- 后面的是你的单元测试名称,可以用中文也可以用英文。
- expect … where …语句块,expect 为核心的测试校验语句块。where 为多个测试用例的列举。
以上测试方法的语义为:列举多个where下面的多个测试用例,以idNo传参,result为结果,调用getBirthAge方法,来验证每条测试用例是否符合我们的预期,添加@Unroll注解,主要是让每一条测试用例都返回测试结果。
如果不加@Unroll注解,那么会把所有的测试用例返回一个结果。
四,Mock
我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。
1,非静态方法Mock
public class UserService
@Autowired
private UserDao userDao;
public UserInfo getUserInfoById(long id)
List<UserInfo> userInfoList = userDao.getAllUserInfo();
for (UserInfo userInfo : userInfoList)
if (userInfo.getId()==id)
return userInfo;
return null;
public class UserDao
public List<UserInfo> getAllUserInfo()
return null;
Junit单测:
@ExtendWith(MockitoExtension.class)
public class UserInfoJunitTest
@Mock
private UserDao userDao;
@InjectMocks
private UserService userService;
@Before
public void before()
MockitoAnnotations.openMocks(this);
@Test
public void userinfoTest()
// 准备参数
List<UserInfo> userInfoList = new ArrayList<>();
UserInfo userInfo1 = new UserInfo();
userInfo1.setId(0);
userInfo1.setName("小明");
UserInfo userInfo2 = new UserInfo();
userInfo1.setId(1);
userInfo1.setName("小强");
userInfoList.add(userInfo1);
userInfoList.add(userInfo2);
// mock数据
Mockito.when(userDao.getAllUserInfo()).thenReturn(userInfoList);
UserInfo userInfo = userService.getUserInfoById(1);
// 验证结果
Assertions.assertEquals(userInfoList.get(1),userInfo);
Spock单测
class UserInfoSpec extends Specification
def userService = new UserService();
def "getUserInfoById"()
given: "准备参数"
def user1 = new UserInfo(id: 0, name: "小明")
def user2 = new UserInfo(id: 1, name: "小强")
and: "mock数据"
def userDao = Mock(UserDao)
Whitebox.setInternalState(userService, "userDao", userDao)
userDao.getAllUserInfo() >> [user1, user2]
when: "方法调用"
def response = userService.getUserInfoById(1)
then: "结果验证"
with(response)
id == 1
name == "小强"
**given … when … then …**语句块:given为条件,when为执行方法,then为结果验证。
when … then 通常是成对出现的,;‘它代表着当执行了 when 块中的操作,会出现 then 块中的期望。
比较明显,上边的JUnit单元测试代码冗余,缺少结构层次,可读性差,随着后续的迭代,势必会导致代码的堆积,维护成本会变得越来越高。下面的单元测试代码Spock会强制要求使用given
、when
、then
这样的语义标签(至少一个),否则编译不通过,这样就能保证代码更加规范,结构模块化,边界范围清晰,可读性强,便于扩展和维护。而且使用了自然语言描述测试步骤,让非技术人员也能看懂测试代码(given
表示输入条件,when
触发动作,then
验证输出结果)。
Spock自带的Mock语法也非常简单:
def userDao = Mock(UserDao)
使用Spock自导的Mock方法构造一个UserDao的Mock对象。
Whitebox.setInternalState(userService, "userDao", userDao)
给userService对象中的userDao属性赋值刚才mock出来的userDao。
userDao.getAllUserInfo() >> [user1, user2]
。
两个右箭头>>
表示模拟userDao.getAllUserInfo()
接口的返回结果,再加上使用的Groovy语言,可以直接使用[]
中括号表示返回的是List
类型。
如果要指定返回多个值的话,可以使用3个右箭头>>>
,比如:studentDao.getAllUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]
。
每次调用userDao.getAllUserInfo()
方法返回不同的值。
2,静态方法Mock
在Spock中可以通过powermock去模拟静态方法、final方法、私有方法等。
但是junit5不支持powermock,只能使用junit4来写
Junit单测
@RunWith(PowerMockRunner.class)
@PrepareForTest(IDNumberUtils.class)
public class IDNumberUtilsStaticTest
@InjectMocks
private UserService userService;
@Before
public void setup()
PowerMockito.mockStatic(IDNumberUtils.class);
@Test
public void testStatic()
// 准备参数
Map<String,String> map = new HashMap<>();
map.put("birthday","1992-08-09");
map.put("age","29");
// mock
PowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map);
int age = userService.getUserAgeByCardId("123");
// 验证结果
Assertions.assertEquals(29,age);
Spock单测:
powermock的PowerMockRunner继承自Junit,所以使用powermock的@PowerMockRunnerDelegate()注解可以指定Spock的父类Sputnik去代理运行power mock,这样就可以在Spock里使用powermock去模拟静态方法、final方法、私有方法等
@PrepareForTest(IDNumberUtils.class)
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
class IDNumberUtilsStaticSpec extends Specification
def userService = new UserService();
void setup()
PowerMockito.mockStatic(IDNumberUtils.class)
def "getAge"()
given: "准备参数"
def map = [birthday: "1992-08-09", age: "29"]
and: "mock"
PowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map)
when: "方法调用"
def respsnse = userService.getUserAgeByCardId("aa")
then: "结果验证"
with(respsnse)
respsnse == 29
当使用powermock来mock静态方法的时候,必须加注解@PrepareForTest
和@RunWith
。注解@PrepareForTest
里写的类是静态方法所在的类。
when
模块里是真正调用要测试方法的入口userService.getUserAgeByCardId()
。
then
模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean
表达式,类似于JUnit的assert
断言机制,但不必显示地写assert
,这也是一种约定优于配置的思想。
then
以上是关于吃透单元测试:Spock单元测试框架的应用与实践的主要内容,如果未能解决你的问题,请参考以下文章