另类 Springboot 集成单元测试:Groovy 脚本测试、零重启、零等待

1,149 阅读3分钟

🗣️什么是 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 功能更丰富,文档、接口测试集成。


VIA:另类 Springboot 集成单元测试:Groovy 脚本测试、零重启、零等待 · 语雀