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。后文简称“框架”。它的使用方法如下:

  1. 在 pom.xml 中加入依赖:
<dependency><groupId>com.lesfurets</groupId><artifactId>jenkins-pipeline-unit</artifactId><version>1.1</version><scope>test</scope>
</dependency>
  1. 写单元测试
// 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 结尾。

  1. 改进 以上代码是为了让读者对共享库脚本的单元测试有更直观的理解。实际工作中会做一些调整。我们会将 extends BasePipelineTestsetUp 方法抽到一个父类中,所有其它测试类继承于它。

此时,我们最简单的共享库的单元测试脚手架就搭建好了。

但是,实际工作中遇到场景并不会这么简单。面对更复杂的场景,必须了解 Jenkins Pipeline Unit testing framework 的原理。由此可见,写单元测试也是需要成本的。至于收益,仁者见仁,智者见智了。

Jenkins Pipeline Unit testing framework 原理

上文中的单元测试实际上做了三件事情:

  1. 加载目标脚本,loadScript 方法由框架提供。
  2. 运行脚本,loadScript 方法返回加载好的脚本。
  3. 断言脚本中的方法是否有按预期执行,helperBasePipelineTest 的一个字段。

从第三步的 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}

它做了三件事情:

  1. 将调用方法名和参数写入到 callStack 中
  2. 如果被调用方法名是被注册了的方法,则执行该方法对象的 mock。下文会详细介绍。
  3. 如果被调用方法没有被注册,则真正执行它。

需要解释一个第二点。并不是所有的共享库中的方法都是需要拦截的。我们只需要对我们感兴趣的方法进行拦截,并实现 mock 的效果。

写到这里,有些读者朋友可能头晕了。笔者在这里进行小结一下。

因为我们不希望共享库脚本中的依赖于 Jenkins 运行时的方法(比如拉代码的步骤)真正运行。所以,我们需要对这些方法进行 mock。在 Groovy 中,我们可以通过方法级别的拦截来实现 mock 的效果。 但是我们又不应该对共享库中所有的方法进行拦截,所以就需要我们在执行单元测试前将自己需要 mock 的方法进行注册到 helper 的 allowedMethodCallbacks 字段中。methodInterceptor拦截器会根据它来进行拦截。

BasePipelineTestsetUp 方法中,框架注册了一些默认方法,不至于我们要手工注册太多方法。以下是部分源码:

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"))

bindingBasePipelineTest 的一个字段,用于绑定变量。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 共享库进行单元测试相关推荐

  1. 周末直播活动|Jenkins共享库实践

    为什么要使用共享库? 其实共享库这并不是一个全新的概念,具有一定编程能力的同学应该清楚一些.例如在编程语言Python中,我们可以将Python代码写到一个文件中,当代码数量增加,我们可以将代码打包成 ...

  2. 搭建jenkins共享库使用Jenkins Shared Library

    参考Jenkins Shared Library(解决多个项目使用同一jenkinsfile问题)_熊吉呜哈哈-CSDN博客

  3. Jenkins 流水线 获取git 分支列表_Jenkins扩展共享库进阶

    读完需 16 分钟 速读需 7 分钟 前言 前面我们介绍了Jenkins多分支流水线.Jenkins流水线即代码之扩展共享库,其实都是"流水线即代码"的体现.我们将Jenkinsf ...

  4. 持续集成:Jenkins Pipeline共享库定义和使用

    通常情况下多个流水线项目需要使用相同的功能,流水线支持创建 "共享库" ,把这些公共的方法类定义在一个仓库中,使多个pipeline项目可以共享这些库,这有助于减少代码冗余. 目录 ...

  5. 「Jenkins Pipeline」- 在 Jenkinsfile 中使用共享库 @20210306

    在Jenkins中,如果共享库被标记为 Load implicitly ,这允许 Pipeline 立即使用共享库中的类和全局变量. 方法一.使用注解(@Library) 要访问其他的共享库,需要在J ...

  6. Linux 下编译安装软件,找不到共享库 xx.so 的解决办法

    编译memcached时,报错没有libevent,于是下载libevent,configure , make && make install ,然后在重新安装memcache成功之后 ...

  7. 在Linux平台上如何使用接静态库和共享库

    1.Linux函数库介绍 函数库可以看做是事先编写的函数集合,它可以与主函数分离,从而增加程序开发的复用性.Linux中函数库可以有3种使用的形式:静态.共享和动态. 1)         静态库的代 ...

  8. 程序员的自我修养--链接、装载与库笔记:Linux共享库的组织

    共享库(Shared Library)概念:其实从文件结构上来讲,共享库和共享对象没什么区别,Linux下的共享库就是普通的ELF共享对象.由于共享对象可以被各个程序之间共享,所以它也就成为了库的很好 ...

  9. Linux上制作可执行的共享库示例

    http://bbs.hadoopor.com/thread-3313-1-1.html x.cpp为共享库libx.so的实现,b.cpp为可执行b的实现. x.cpp文件内容: #include ...

最新文章

  1. 第0篇 面向对象思想
  2. Java实现有序数组和无序数组_【算法】字典的诞生:有序数组 PK 无序链表
  3. 造轮子是什么意思_精炼:我造轮子的秘诀
  4. ++和--操作符分析
  5. 【Python】在模仿中精进数据可视化09:近期基金涨幅排行可视化
  6. Linux服务器上新增开放端口号
  7. parallelstudio license 位置_卫生间这3个位置95%装修没用好
  8. 【Python】PIL库介绍
  9. 揭秘 AWS 基础架构底层运维和构建之道!
  10. 开始学习C#.Net
  11. Linux用php上传表单文件,文件太大提示[413 Request Entity Too Large]
  12. PDF转图片文字丢失问题解决
  13. 微信支付:小微商户申请入驻第一步:平台证书序列号的获取
  14. paypal接入指南
  15. D. Three Religions
  16. ArcGIS系列(一):DEM数字高程模型数据的生成
  17. java报错root cause_[Filtered request failed.] with root cause java.io.OptionalDataException
  18. 简单的Java 16方格排序游戏
  19. 安装和配置 苹果CMS v10 的记录 搭建教程
  20. Google GMS认证测试几个名词

热门文章

  1. 好友消息和群消息区别
  2. 各种封装——封装getClass
  3. hdu3652(数位dp)
  4. 网站发布错误“遭遇战”
  5. URLConnection和HttpURLConnection类
  6. oracle rac实例切换,RAC+单实例DG的切换
  7. python内存池机制_python的内存管理机制
  8. c++思维导图_必看|用好思维导图,别神话思维导图
  9. idea 2020.2 如何设置classpath_开发属于自己的第一款IDEA插件!
  10. css设置字体颜色、文本对齐方式、首行缩进、文本装饰、列表样式、鼠标样式、禁止文本域拖拽、轮廓线、块级元素对齐方式、文字溢出设置