参考
最近发现了一种写法简洁高效,一个单测方法可以测试多组测试数据,且测试结果一目了然的单测框架Spock。Spock国外的测试框架,其设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试。尽管Spock写单测,需要使用groovy语言,但是groovy语言是一种弱类型,写法超级简单,我也是零基础的groovy新手,相信你看过这篇文档,就会用groovy写单测啦。
环境配置
引入jar包
<!--groovy单测框架-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all-tests</artifactId>
<version>2.0.0-rc-3</version>
</dependency>
<!-- Mandatory dependencies for using Spock test framework -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.4</version>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.4</version>
<scope>test</scope>
</dependency>
配置插件
<plugin>
<!--groovy plugin-->
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.4</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- spock单测文件路径 -->
<testSources>
<testSource>
<directory>${project.basedir}/src/test/java</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
<testSource>
<directory>${project.basedir}/src/test/groovy</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
</testSources>
</configuration>
</plugin>
Spock用法
0、被测试类
public class TaskService {
/**
* 环境
*/
@Value("${spring.current.env}")
private String env;
/**
* 任务 服务类
*/
@Resource
private ITaskRepository taskRepository;
/**
* 智能配置 管理类
*/
@Resource
private IntelligentConfigManager configManager;
/**
* 查询任务信息(根据环境,查询任务)
*
* @return 任务
*/
public Result<Task> getTask() {
if (EnvEnum.isDaily(env)) {
// 日常环境,任务取值 本方法直接new
Task task = new Task();
task.setInput(EnvEnum.DAILY.name() + " 任务");
return Result.isOk(task);
}
if (EnvEnum.isPre(env)) {
// 预发环境,任务取值于 本类的方法的返回值
return getPreTask();
}
try {
// 线上环境,任务取值于 另一个类的方法的返回值
return Result.isOk(taskRepository.getTask(1L));
} catch (Exception ex) {
// 异常
return Result.onError("异常任务");
}
}
/**
* 查询智能配置信息
*
* @param query 查询智能配置信息的query
* @return 智能配置信息集合
*/
public Result<List<IntelligentConfigDTO>> getAllIntelligentConfigDTOList(IntelligentConfigQuery query) {
List<IntelligentConfigDTO> allConfigDTOList = Lists.newArrayList();
// 查询全部的智能配置信息
query.setPage(1);
PageResult<IntelligentConfigDTO> intelligentConfigDTOPageResult = configManager.queryList(query);
while (intelligentConfigDTOPageResult.isSuccessful()
&& !CollectionUtils.isEmpty(intelligentConfigDTOPageResult.getList())) {
allConfigDTOList.addAll(Lists.newArrayList(intelligentConfigDTOPageResult.getList()));
query.setPage(query.getPage() + 1);
intelligentConfigDTOPageResult = configManager.queryList(query);
}
return Result.isOk(allConfigDTOList);
}
/**
* 查询预发环境的任务
*
* @return 任务
*/
public Result<Task> getPreTask() {
Task task = new Task();
task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());
return Result.isOk(task);
}
}
1、given-expect-where
given块:用于写测试前的准备工作,例如mock方法的返回值等
expect块:只能写判断式,如a==b
where块:用于写断言(测试数据、及期望返回值),where块可以写多组测试数据、和期望返回值
@Unroll:通过“#”可以动态获取where块的数据,测试结果一目了然,看例子,更容易理解
import com.google.common.collect.Lists
import spock.lang.Specification
import spock.lang.Unroll
class TaskServiceSpockTest extends Specification {
// mock TaskService的属性
ITaskRepository taskRepository = Mock()
IntelligentConfigManager configManager = Mock()
// 要测试的类
TaskService taskService = new TaskService(taskRepository: taskRepository, configManager: configManager)
void setup() {
// 也可以在setup中,给TaskService的属性赋值
// taskTestService.taskRepository = taskRepository
// taskTestService.configManager = configManager
}
@Unroll
def "testGetTask 环境=#env, 任务包含关键字=#keyWord, 任务是否包含关键字=#result"() {
given: "测试前的准备:给taskService的env赋值"
taskService.env = env
and: "mock taskRepository.getTask(_) 的返回值"
Task task = new Task();
task.setInput(EnvEnum.PRODUCT.name())
taskRepository.getTask(_) >> task
and: "执行taskService.getTask()"
Result<Task> taskResult = taskService.getTask()
println(taskResult)
expect: "expect只能写判断式,断言测试结果"
result == taskResult.getData().getInput().contains(keyWord)
where: "测试数据、及测试结果"
env | keyWord | result
EnvEnum.DAILY.getVal() | EnvEnum.DAILY.name() | true
EnvEnum.PRE.getVal() | EnvEnum.PRE.name() | true
EnvEnum.PRODUCT.getVal() | EnvEnum.PRODUCT.name() | true
}
}
执行结果
看到这里,你是不是很兴奋,一个单测就能覆盖到被测方法的各个逻辑,且测试结果一目了然。
2、given-when-then
除了given-expect-where,还可以使用given-when-then。
given块:用于写测试前的准备工作,例如mock方法的返回值等
when块:执行被测试方法
then块:用于写断言,如==、不会抛出异常noExceptionThrown()、方法被调用的次数0 * taskRepository.getTask(_)
def "testGetTaskWhen"() {
given: "测试前的准备: mock taskRepository.getTask(_)的返回值"
Task task = new Task();
task.setInput(EnvEnum.PRODUCT.name())
taskRepository.getTask(_) >> task
and: "给taskService的env赋值"
taskService.env = EnvEnum.PRODUCT.getVal()
when: "执行被测试方法"
Result<Task> result = taskService.getTask()
println(result)
then: "断言"
// 断言:返回结果是true
result.isSuccessful() == true
// 断言:不会抛出异常
noExceptionThrown()
}
3、mock异常
com.alibaba.polystar.service.intelligent.service.TaskService#getTask有try-catch,那么怎么覆盖掉catch的逻辑呢?下面讲下,如何mock异常。
@Unroll
def "testGetTaskException 环境=#env, 结果=#result"() {
given: "测试前的准备:给taskService的env赋值"
taskService.env = env
and: "mock taskRepository.getTask(_) 抛出异常"
taskRepository.getTask(_) >> { throw new Exception() }
and: "执行被测试方法"
Result<Task> result1 = taskService.getTask()
println(result1)
expect: "expect只能是判断式:断言 测试结果"
result == result1.isSuccessful()
// 测试数据、测试结果断言
where:
env | result
EnvEnum.DAILY.getVal() | true
EnvEnum.PRE.getVal() | true
EnvEnum.PRODUCT.getVal() | false
}
通过given-when-then测试抛出异常,不能像where能测试多组测试数据。
def "testGetTaskWhen 异常"() {
given: "测试前的准备: mock taskRepository.getTask(_)抛出运行时异常"
taskRepository.getTask(_) >> { throw new RuntimeException() }
and: "给taskService的env赋值"
taskService.env = EnvEnum.PRODUCT.getVal()
when: "执行被测试方法"
Result<Task> result1 = taskService.getTask()
println(result1)
then: "断言测试结果"
result1.isSuccessful() == false
}
4、mock方法每次的返回值不一样
在日常开发中,可能会遇到while查询某个方法,直到某种条件,才会break,如TaskService.getAllIntelligentConfigDTOList。为了测试这样的逻辑,就需要使每次mock方法的返回值不同。
def "testGetAllIntelligentConfigDTOList"() {
given: "测试前的准备"
// 第一次调,返回 长度=1的集合
IntelligentConfigDTO configDTO = new IntelligentConfigDTO();
configDTO.setId(1L)
com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult =
PageResult.build(1, 1, 1, Lists.newArrayList(configDTO))
// 第二次调,返回 空集合,使while循环结束
com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult2 =
PageResult.build(2, 1, 0, Lists.newArrayList())
// 模拟方法调多次时,返回的结果
configManager.queryList(_) >> pageResult >> pageResult2
// 执行被测试方法
IntelligentConfigQuery query = new IntelligentConfigQuery();
Result<List<IntelligentConfigDTO>> result = taskService.getAllIntelligentConfigDTOList(query)
println(result)
// 智能配置的总条数
def size = result.getData().size()
expect: "expect只能是判断式:断言 测试结果,断言智能配置size=1"
size == 1
}
5、mock本类方法
在日常开发中,被测试方法A调用了同类的方法B,而B方法逻辑复杂,如getPreTask()方法,会调用本类的getInternalTask(),这时可以通过spy来mock本类方法getInternalTask(),来编写getPreTask()方法的单测。TaskService taskService = Spy()的作用是,如果TaskService的方法没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法。
/**
* 查询预发环境的任务
*
* @return 任务
*/
public Result<Task> getPreTask() {
return Result.isOk(getInternalTask());
}
/**
* 查询 内部 任务
*
* @return 任务
*/
public Task getInternalTask() {
Task task = new Task();
task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());
return task;
}
单测
def "testGetPreTask"() {
given: "测试前的准备"
// 通过spy创建TaskService,TaskService的方法如果没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法
TaskService taskService = Spy();
and: "mock 本类的的方法"
Task task = new Task();
task.setInput("spy getInternalTask 任务");
taskService.getInternalTask() >> task
and: "执行被测试方法"
Result<Task> result = taskService.getPreTask()
println(result)
expect: "expect只能是判断式:断言测试结果"
result.getData().getInput().contains("spy") == true
}
最后
看到这里,是不是你也觉得spock语法非常简洁、功能非常强大,那就快快使用起来吧。