内容简介:layout: post title: “如何对 Jenkins 共享库进行单元测试” description: “” date: 2019-05-25 tags: [Jenkins] comments: true share: true —Jenkins 共享库是除了 Jenkins 插件外,另一种扩展 Jenkins 流水线的技术。通过它,可以定义轻松的自定义步骤,还可以对现有的流水线逻辑进行一定程度的抽象与封装。至于如何写及如何使用它,读者朋友可以移步附录中的官方文档。
layout: post title: “如何对 Jenkins 共享库进行单元测试” description: “” date: 2019-05-25 tags: [Jenkins] comments: true share: true —
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 中加入依赖: ```xml
2. 写单元测试 ```groovy // test/groovy/codes/showme/pipeline/lib/SayHelloTest.groovy // 必须继承 BasePipelineTest 类 class SayHelloTest extends BasePipelineTest { @Override @Before public void setUp() throws Exception { // 告诉框架,共享库脚本所在的目录 scriptRoots = ["vars"] // 初始化框架 super.setUp() } @Test void 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 stack int depth = Thread.currentThread().stackTrace.findAll { it.className == delegate.class.name }.size() this.registerMethodCall(delegate, depth, name, args) // check if it is to be intercepted def intercepted = this.getAllowedMethodEntry(name, args) if (intercepted != null && intercepted.value) { intercepted.value.delegate = delegate return callClosure(intercepted.value, args) } // if not search for the method declaration MetaMethod m = delegate.metaClass.getMetaMethod(name, args) // ...and call it. If we cannot find it, delegate call to methodMissing def 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 的零侵入拦截,以实现一些平台级特殊的需求。
附录
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网页设计创意书(卷2)
麦克尼尔 / 图灵编辑部 / 人民邮电 / 2012-1 / 49.00元
《网页设计创意书(卷2)》是《网页设计创意书》的卷2,但并非其简单补充,而是作者基于近几年网站发展新趋势的再创作。《网页设计创意书(卷2)》先讲解了如何从他人的优秀设计中寻找灵感,接着阐述了重点、对比、平衡、对齐等网站设计的基本原则,然后将网站按类型、设计元素、风格和主题、结构样式和结构元素分类,并分章介绍了每一类的设计技巧。《网页设计创意书(卷2)》语言简练,结合作者精挑细选的网站实例,通俗易懂......一起来看看 《网页设计创意书(卷2)》 这本书的介绍吧!