如何对 Jenkins 共享库进行单元测试
2019独角兽企业重金招聘Python工程师标准>>>
本文首发于:Jenkins 中文社区
Jenkins 共享库是除了 Jenkins 插件外,另一种扩展 Jenkins 流水线的技术。通过它,可以轻松地自定义步骤,还可以对现有的流水线逻辑进行一定程度的抽象与封装。至于如何写及如何使用它,读者朋友可以移步附录中的官方文档。
对共享库进行单元测试的原因
但是如何对它进行单元测试呢?共享库越来越大时,你不得不考虑这个问题。因为如果你不在早期就开始单元测试,共享库后期可能就会发展成如下图所示的“艺术品”——能工作,但是脆弱到没有人敢动。
[图片来自网络,侵权必删]
这就是代码越写越慢的原因之一。后人要不断地填前人有意无意挖的坑。
共享库单元测试搭建
共享库官方文档介绍的代码仓库结构
(root)
+- src # Groovy source files
| +- org
| +- foo
| +- Bar.groovy # for org.foo.Bar class
+- vars
| +- foo.groovy # for global 'foo' variable
| +- foo.txt # help for 'foo' variable
+- resources # resource files (external libraries only)
| +- org
| +- foo
| +- bar.json # static helper data for org.foo.Bar
以上是共享库官方文档介绍的代码仓库结构。整个代码库可以分成两部分:src 目录部分和 vars 目录部分。它们的测试脚手架的搭建方式是不一样的。
src 目录中的代码与普通的 Java 类代码本质上没有太大的区别。只不过换成了 Groovy 类。
但是 vars 目录中代码本身是严重依赖于 Jenkins 运行时环境的脚本。
接下来,分别介绍如何搭建它们的测试脚手架。
测试 src 目录中的 Groovy 代码
在对 src 目录中的 Groovy 代码进行单元测试前,我们需要回答一个问题:使用何种构建工具进行构建?
我们有两种常规选择:Maven 和 Gradle。本文选择的是前者。
接下来的第二个问题是,共享库源代码结构并不是 Maven 官方标准结构。下例为标准结构:
├── pom.xml
└── src├── main│ ├── java│ └── resources└── test├── java└── resources
因为共享库使用的 Groovy 写的,所以,还必须使 Maven 能对 Groovy 代码进行编译。
可以通过 Maven 插件:GMavenPlus 解决以上问题,插件的关键配置如下:
<configuration><sources><source><!-- 指定Groovy类源码所在的目录 --><directory>${project.basedir}/src</directory><includes><include>**/*.groovy</include></includes></source></sources><testSources><testSource><!-- 指定单元测试所在的目录 --><directory>${project.basedir}/test/groovy</directory><includes><include>**/*.groovy</include></includes></testSource></testSources>
</configuration>
同时,我们还必须加入 Groovy 语言的依赖:
<dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all</artifactId><version>${groovy-all.version}</version>
</dependency>
最终目录结构如下图所示:
然后我们就可以愉快地对 src 目录中的代码进行单元测试了。
测试 vars 目录中 Groovy 代码
对 vars 目录中的脚本的测试难点在于它强依赖于 Jenkins 的运行时环境。换句话说,你必须启动一个 Jenkins 才能正常运行它。但是这样就变成集成测试了。那么怎么实现单元测试呢?
经 Google 发现,前人已经写了一个 Jenkins 共享库单元测试的框架。我们拿来用就好。所谓,前人载树,后人乘凉。
这个框架叫:Jenkins Pipeline Unit testing framework。后文简称“框架”。它的使用方法如下:
- 在 pom.xml 中加入依赖:
<dependency><groupId>com.lesfurets</groupId><artifactId>jenkins-pipeline-unit</artifactId><version>1.1</version><scope>test</scope>
</dependency>
- 写单元测试
// test/groovy/codes/showme/pipeline/lib/SayHelloTest.groovy
// 必须继承 BasePipelineTest 类
class SayHelloTest extends BasePipelineTest {@Override@Beforepublic void setUp() throws Exception {// 告诉框架,共享库脚本所在的目录scriptRoots = ["vars"]// 初始化框架super.setUp()}@Testvoid call() {// 加载脚本def script = loadScript("sayHello.groovy")// 运行脚本script.call()// 断言脚本中运行了 echo 方法// 同时参数为"hello pipeline"assertThat(helper.callStack.findAll { c -> c.methodName == 'echo' }.any { c -> c.argsToString().contains('hello pipeline') }).isTrue()// 框架提供的方法,后面会介绍。printCallStack()}
}
创建单元测试时,注意选择 Groovy 语言,同时类名要以
Test
结尾。
- 改进 以上代码是为了让读者对共享库脚本的单元测试有更直观的理解。实际工作中会做一些调整。我们会将
extends BasePipelineTest
和setUp
方法抽到一个父类中,所有其它测试类继承于它。
此时,我们最简单的共享库的单元测试脚手架就搭建好了。
但是,实际工作中遇到场景并不会这么简单。面对更复杂的场景,必须了解 Jenkins Pipeline Unit testing framework 的原理。由此可见,写单元测试也是需要成本的。至于收益,仁者见仁,智者见智了。
Jenkins Pipeline Unit testing framework 原理
上文中的单元测试实际上做了三件事情:
- 加载目标脚本,
loadScript
方法由框架提供。 - 运行脚本,
loadScript
方法返回加载好的脚本。 - 断言脚本中的方法是否有按预期执行,
helper
是BasePipelineTest
的一个字段。
从第三步的 helper.callStack
中,我们可以猜到第二步中的script.call()
并不是真正的执行,而是将脚本中方法调用被写到 helper 的 callStack 字段中。从 helper 的源码可以确认这一点:
/**
* Stack of method calls of scripts loaded by this helper
*/
List<MethodCall> callStack = []
那么,script.call() 内部是如何做到将方法调用写入到 callStack 中的呢?
一定是在 loadScript
运行过程做了什么事情,否则,script 怎么会多出这些行为。我们来看看它的底层源码:
/*** Load the script with given binding context without running, returning the Script* @param scriptName* @param binding* @return Script object*/Script loadScript(String scriptName, Binding binding) {Objects.requireNonNull(binding, "Binding cannot be null.")Objects.requireNonNull(gse, "GroovyScriptEngine is not initialized: Initialize the helper by calling init().")Class scriptClass = gse.loadScriptByName(scriptName)setGlobalVars(binding)Script script = InvokerHelper.createScript(scriptClass, binding)script.metaClass.invokeMethod = getMethodInterceptor()script.metaClass.static.invokeMethod = getMethodInterceptor()script.metaClass.methodMissing = getMethodMissingInterceptor()return script}
gse
是 Groovy 脚本执行引擎 GroovyScriptEngine
。它在这里的作用是拿到脚本的 Class 类型,然后使用 Groovy 语言的 InvokerHelper
静态帮助类创建一个脚本对象。
接下来做的就是核心了:
script.metaClass.invokeMethod = getMethodInterceptor()
script.metaClass.static.invokeMethod = getMethodInterceptor()
script.metaClass.methodMissing = getMethodMissingInterceptor()
它将脚本对象实例的方法调用都委托给了拦截器 methodInterceptor
。Groovy 对元编程非常友好。可以直接对方法进行拦截。拦截器源码如下:
/*** Method interceptor for any method called in executing script.* Calls are logged on the call stack.*/public methodInterceptor = { String name, Object[] args ->// register method call to stackint depth = Thread.currentThread().stackTrace.findAll { it.className == delegate.class.name }.size()this.registerMethodCall(delegate, depth, name, args)// check if it is to be intercepteddef intercepted = this.getAllowedMethodEntry(name, args)if (intercepted != null && intercepted.value) {intercepted.value.delegate = delegatereturn callClosure(intercepted.value, args)}// if not search for the method declarationMetaMethod m = delegate.metaClass.getMetaMethod(name, args)// ...and call it. If we cannot find it, delegate call to methodMissingdef result = (m ? this.callMethod(m, delegate, args) : delegate.metaClass.invokeMissingMethod(delegate, name, args))return result}
它做了三件事情:
- 将调用方法名和参数写入到 callStack 中
- 如果被调用方法名是被注册了的方法,则执行该方法对象的 mock。下文会详细介绍。
- 如果被调用方法没有被注册,则真正执行它。
需要解释一个第二点。并不是所有的共享库中的方法都是需要拦截的。我们只需要对我们感兴趣的方法进行拦截,并实现 mock 的效果。
写到这里,有些读者朋友可能头晕了。笔者在这里进行小结一下。
因为我们不希望共享库脚本中的依赖于 Jenkins 运行时的方法(比如拉代码的步骤)真正运行。所以,我们需要对这些方法进行 mock。在 Groovy 中,我们可以通过方法级别的拦截来实现 mock 的效果。 但是我们又不应该对共享库中所有的方法进行拦截,所以就需要我们在执行单元测试前将自己需要 mock 的方法进行注册到 helper 的
allowedMethodCallbacks
字段中。methodInterceptor
拦截器会根据它来进行拦截。
在 BasePipelineTest
的 setUp
方法中,框架注册了一些默认方法,不至于我们要手工注册太多方法。以下是部分源码:
helper.registerAllowedMethod("sh", [Map.class], null)
helper.registerAllowedMethod("checkout", [Map.class], null)
helper.registerAllowedMethod("echo", [String.class], null)
registerAllowedMethod
各参数的作用:
- 第一个参数:要注册的方法。
- 第二参数:该方法的参数列表。
- 第三参数:一个闭包。当该访问被调用时会执行此闭包。
以上就是框架的基本原理了。接下来,再介绍几种场景。
几种应用场景
环境变量
当你的共享库脚本使用了 env
变量,可以这样测试:
binding.setVariable('env', new HashMap())
def script = loadScript('setEnvStep.groovy')
script.invokeMethod("call", [k: '123', v: "456"])
assertEquals("123", ((HashMap) binding.getVariable("env")).get("k"))
binding
由 BasePipelineTest
的一个字段,用于绑定变量。binding
会被设置到 gse
中。
调用其它共享库脚本
比如脚本 a 中调用到了 setEnvStep
。这时可以在 a 执行前注册 setEnvStep
方法。
helper.registerAllowedMethod("setEnvStep", [LinkedHashMap.class], null)
希望被 mock 的方法能有返回值
helper.registerAllowedMethod("getDevOpsMetadata", [String.class, String.class], {return "data from cloud"
})
后记
不得不说 Jenkins Pipeline Unit testing framework 框架的作者非常聪明。另外,此类技术不仅可以用于单元测试。理论上还可以用于 Jenkins pipeline 的零侵入拦截,以实现一些平台级特殊的需求。
附录
- 共享库官方文档:https://jenkins.io/zh/doc/book/pipeline/shared-libraries/
- 本文示例代码:https://github.com/zacker330/jenkins-pipeline-shared-lib-unittest-demo
- JenkinsPipelineUnit:https://github.com/jenkinsci/JenkinsPipelineUnit
作者:翟志军
转载于:https://my.oschina.net/jenkinszh/blog/3055265
如何对 Jenkins 共享库进行单元测试相关推荐
- 周末直播活动|Jenkins共享库实践
为什么要使用共享库? 其实共享库这并不是一个全新的概念,具有一定编程能力的同学应该清楚一些.例如在编程语言Python中,我们可以将Python代码写到一个文件中,当代码数量增加,我们可以将代码打包成 ...
- 搭建jenkins共享库使用Jenkins Shared Library
参考Jenkins Shared Library(解决多个项目使用同一jenkinsfile问题)_熊吉呜哈哈-CSDN博客
- Jenkins 流水线 获取git 分支列表_Jenkins扩展共享库进阶
读完需 16 分钟 速读需 7 分钟 前言 前面我们介绍了Jenkins多分支流水线.Jenkins流水线即代码之扩展共享库,其实都是"流水线即代码"的体现.我们将Jenkinsf ...
- 持续集成:Jenkins Pipeline共享库定义和使用
通常情况下多个流水线项目需要使用相同的功能,流水线支持创建 "共享库" ,把这些公共的方法类定义在一个仓库中,使多个pipeline项目可以共享这些库,这有助于减少代码冗余. 目录 ...
- 「Jenkins Pipeline」- 在 Jenkinsfile 中使用共享库 @20210306
在Jenkins中,如果共享库被标记为 Load implicitly ,这允许 Pipeline 立即使用共享库中的类和全局变量. 方法一.使用注解(@Library) 要访问其他的共享库,需要在J ...
- Linux 下编译安装软件,找不到共享库 xx.so 的解决办法
编译memcached时,报错没有libevent,于是下载libevent,configure , make && make install ,然后在重新安装memcache成功之后 ...
- 在Linux平台上如何使用接静态库和共享库
1.Linux函数库介绍 函数库可以看做是事先编写的函数集合,它可以与主函数分离,从而增加程序开发的复用性.Linux中函数库可以有3种使用的形式:静态.共享和动态. 1) 静态库的代 ...
- 程序员的自我修养--链接、装载与库笔记:Linux共享库的组织
共享库(Shared Library)概念:其实从文件结构上来讲,共享库和共享对象没什么区别,Linux下的共享库就是普通的ELF共享对象.由于共享对象可以被各个程序之间共享,所以它也就成为了库的很好 ...
- Linux上制作可执行的共享库示例
http://bbs.hadoopor.com/thread-3313-1-1.html x.cpp为共享库libx.so的实现,b.cpp为可执行b的实现. x.cpp文件内容: #include ...
最新文章
- 第0篇 面向对象思想
- Java实现有序数组和无序数组_【算法】字典的诞生:有序数组 PK 无序链表
- 造轮子是什么意思_精炼:我造轮子的秘诀
- ++和--操作符分析
- 【Python】在模仿中精进数据可视化09:近期基金涨幅排行可视化
- Linux服务器上新增开放端口号
- parallelstudio license 位置_卫生间这3个位置95%装修没用好
- 【Python】PIL库介绍
- 揭秘 AWS 基础架构底层运维和构建之道!
- 开始学习C#.Net
- Linux用php上传表单文件,文件太大提示[413 Request Entity Too Large]
- PDF转图片文字丢失问题解决
- 微信支付:小微商户申请入驻第一步:平台证书序列号的获取
- paypal接入指南
- D. Three Religions
- ArcGIS系列(一):DEM数字高程模型数据的生成
- java报错root cause_[Filtered request failed.] with root cause java.io.OptionalDataException
- 简单的Java 16方格排序游戏
- 安装和配置 苹果CMS v10 的记录 搭建教程
- Google GMS认证测试几个名词
热门文章
- 好友消息和群消息区别
- 各种封装——封装getClass
- hdu3652(数位dp)
- 网站发布错误“遭遇战”
- URLConnection和HttpURLConnection类
- oracle rac实例切换,RAC+单实例DG的切换
- python内存池机制_python的内存管理机制
- c++思维导图_必看|用好思维导图,别神话思维导图
- idea 2020.2 如何设置classpath_开发属于自己的第一款IDEA插件!
- css设置字体颜色、文本对齐方式、首行缩进、文本装饰、列表样式、鼠标样式、禁止文本域拖拽、轮廓线、块级元素对齐方式、文字溢出设置