🗣️什么是 Springboot 集成测试?
需要测试 Spring 管理的 bean 中的方法时,就需要用到 Spring 集成单测。
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class FooSpringTest {
@Test
public void xxx() {
//...
}
}
🗣️怎么零重启、零等待?
上面所示的传统集成方式,想做到零重启、零等待,几乎是不可能的!!
就算您提前将所有想要测试的 case 全部编写好了,然后启动一次,跑完所有测试用例。
但是,用例总有不通过的吧,不通过的用例,对应的代码,进行修正,修正后,再次跑一遍用例。
项目复杂后,启动耗时甚至分钟级别的。加上,修 bug 很多时候,是反反复复,改完一个,冒出一个……
痛苦指数,自己清楚呢~
🗣️另类 Springboot 集成单元测试
-
Groovy 脚本执行单测用例 --零等待
-
JRebel 热部署插件 --零重启 (或其他支持热部署的都可以)
下面详细介绍,如何使用 Groovy 脚本执行单元测试用例。
使用步骤
1 引入Groovy 脚本引擎相关依赖
我这里不贴了,不记得了😜回头补上。
2 定义一个 http 接口
/**
* !!!请求头设置:Content-Type=text/plain !!!
* @param params 脚本内容
*/
@AdminApi //接口调用权限控制的定制切面注解
@PostMapping("/invoke/groovy")
public Object invokeGroovy(@RequestBody String params) {
String groovy = params;
Object eval = null;
try {
// 执行脚本,GroovyEngine 见下文
eval = GroovyEngine.eval(groovy);
log.info("invoke groovy ==>\n{}", JSON.toJSONString(eval));
} catch (Exception e) {
String errmsg = "GroovyEngine.eval error: " + e.getMessage();
log.error(errmsg, e);
return Result.fail(errmsg)
.appendError(String.valueOf(e.getCause()));
}
return eval;
}
import javax.script.*;
/**
* <p>
*
* @author L&J
* @date 2021/10/12 3:46 下午
*/
public class GroovyEngine {
protected static final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
protected static final String GROOVY = "groovy";
public static Object eval(String script) {
return eval(script, null);
}
/**
* 直接编译 script
*/
public static Object eval(String script, ScriptContext context) {
ScriptEngine engine = scriptEngineManager.getEngineByName(GROOVY);
Object res = null;
try {
if (context != null) {
res = engine.eval(script, context);
} else {
res = engine.eval(script);
}
} catch (ScriptException e) {
throw new RuntimeException("eval script 异常", e);
}
return res;
}
public static Invocable getEngine(String script) {
return getEngine(script, null);
}
/**
* 编译 script 获取 engine
*/
public static Invocable getEngine(String script, ScriptContext context) {
ScriptEngine engine = scriptEngineManager.getEngineByName(GROOVY);
try {
if (context != null) {
engine.eval(script, context);
} else {
engine.eval(script);
}
return (Invocable) engine;
} catch (ScriptException e) {
throw new RuntimeException("eval script 异常", e);
}
}
}
3 使用演示
上面步骤,就可以使用核心功能了。每次改完 bug 后,热部署下。然后,Postman 之类的工具,再调下接口,验证。
下面,进一步介绍,提高使用体验。
简化使用技巧
🗣️针对想要测试 Spring Component ,创建对应的 Groovy 脚本文件
自动在 test 下新建 TestServiceTest.groovy 文件
在这脚本里编写测试用例代码。好处是利用编辑器的代码提示!!
import cn.hutool.extra.spring.SpringUtil
import com.sankuai.groceryrisk.common.experiment.Jest
import com.sankuai.groceryrisk.risk.investigation.service.service.impl.TestService
//获取容器中的 bean, 这里 SpringUtil 用的 hutool 工具库的, 请在启动类上 import 下
def bean = SpringUtil.getBean(TestService.class)
def jest = Jest.instance()
jest.test({
//调用方法, 返回值会自动作为 http 结果返回, groovy 语言最后一行自动就是返回值
bean.testServ1("aa", 123)
})
注:Jest 是辅助的一个工具类。
输出:
{
"results": {
"Test Case 1": {
"r2": "hello",
"r1": 111
}
}
}
总结
于是,另类单测流程就是:
- 编写业务类代码
- 创建单测类,以前是创建 JUnit 测试类,现在改为创建 Groovy 脚本,使用 Jest 工具类归类测试用例。
- 执行单测,以前是编辑器里启动单测代码,现在改为复制粘贴 Groovy 脚本文本,调用脚本测试接口。
- 修 bug 后,热部署
- …… 重复过程
附录
import cn.hutool.core.exceptions.ExceptionUtil;
import groovy.lang.Closure;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.StringJoiner;
/**
* 方便脚本测试
* <p>
* 示例: example.groovy
* <pre>
* def jest = Jest.instance()
*
*
* jest.one("test PunishContentVO", {
* Stream.of(PunishContentEnum.values())
* .map({
* return new PunishContentVO(it, null)
* })
* .collect(Collectors.toList())
* })
*
* jest.one("test PunishContentVO 2", {
* Arrays.stream(PunishContentEnum.values())
* .map({
* return it.toString() + " isNone ? " + new PunishContentVO(it, null).isNone()
* })
* .collect(Collectors.toList())
* })
*
* // 或, 匿名, 多个 test case, 自动命名如 "Test Case 1"
* jest.test({
* println "可以打印东西吗 ===================="
* "aa"
* }, {
* "bb"
* }, {
* "cc"
* })
*
* jest.getResults()
* </pre>
* @author L&J
* @version 0.1
* @since 2022/11/25 18:02
*/
public class Jest implements Serializable {
private final LinkedHashMap<String, Object> results = new LinkedHashMap<>();
/**
* @param name 用例名
* @param callable groovy Closure 类型, 不用jdk的lambda, 是因为, 可变入参时, 脚本引擎貌似不支持
* @return Jest
*/
public Jest one(String name, Closure<?> callable) {
try {
Object r = callable.call();
results.put(name, r);
} catch (Exception ex) {
results.put(name, ExceptionUtil.stacktraceToString(ex));
}
return this;
}
public Jest test(Closure<?>... callables) {
if (callables != null) {
for (int i = 0; i < callables.length; i++) {
Closure<?> callable = callables[i];
one("Test Case " + (i+1), callable);
}
}
return this;
}
public LinkedHashMap<String, Object> getResults() {
return results;
}
@Override
public String toString() {
return new StringJoiner(", ", Jest.class.getSimpleName() + "[", "]")
.add("results=" + results)
.toString();
}
public static Jest instance() {
return new Jest();
}
}
另,本文使用的 Apifox 作为请求的测试工具,推荐,比 Postman 功能更丰富,文档、接口测试集成。